diff --git a/tools/analyzer_plugin/README.md b/tools/analyzer_plugin/README.md index e5d3fa23b..d6595f250 100644 --- a/tools/analyzer_plugin/README.md +++ b/tools/analyzer_plugin/README.md @@ -120,7 +120,7 @@ Currently-available debug info: builder and analyzer functionality dealing with component declarations - `// debug: over_react_metrics` - shows performance data on how long diagnostics took to run - `// debug: over_react_exhaustive_deps` - shows info on how dependencies were detected/interpreted -- `// debug: over_react_required_props` - shows requiredness info for all props for each builder +- `// debug: over_react_required_props` - shows requiredness info for all props for each builder, as well as prop forwarding info #### Attaching a Debugger The dev experience when working on this plugin isn't ideal (See the `analyzer_plugin` debugging docs [for more information](https://github.com/dart-lang/sdk/blob/master/pkg/analyzer_plugin/doc/tutorial/debugging.md)), but it's possible debug and see logs from the plugin. diff --git a/tools/analyzer_plugin/analysis_options.yaml b/tools/analyzer_plugin/analysis_options.yaml index cac19697a..f11db5869 100644 --- a/tools/analyzer_plugin/analysis_options.yaml +++ b/tools/analyzer_plugin/analysis_options.yaml @@ -3,6 +3,7 @@ include: package:workiva_analysis_options/v1.recommended.yaml analyzer: exclude: - playground/** + - test/temporary_test_fixtures/** - test/test_fixtures/** strong-mode: implicit-casts: false diff --git a/tools/analyzer_plugin/lib/src/diagnostic/missing_required_prop.dart b/tools/analyzer_plugin/lib/src/diagnostic/missing_required_prop.dart index b586fd5fa..8f249fa2b 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/missing_required_prop.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/missing_required_prop.dart @@ -7,6 +7,7 @@ import 'package:over_react_analyzer_plugin/src/util/ast_util.dart'; import 'package:over_react_analyzer_plugin/src/util/null_safety_utils.dart'; import 'package:over_react_analyzer_plugin/src/util/pretty_print.dart'; import 'package:over_react_analyzer_plugin/src/util/prop_declarations/props_set_by_factory.dart'; +import 'package:over_react_analyzer_plugin/src/util/prop_forwarding/forwarded_props.dart'; import 'package:over_react_analyzer_plugin/src/util/util.dart'; import 'package:over_react_analyzer_plugin/src/util/weak_map.dart'; @@ -67,6 +68,7 @@ class MissingRequiredPropDiagnostic extends ComponentUsageDiagnosticContributor "Missing required late prop {0}.", AnalysisErrorSeverity.WARNING, AnalysisErrorType.STATIC_WARNING, + correction: _correctionMessage, ); // Note: this code is disabled by default in getDiagnosticContributors @@ -76,8 +78,12 @@ class MissingRequiredPropDiagnostic extends ComponentUsageDiagnosticContributor "Missing @requiredProp {0}.", AnalysisErrorSeverity.INFO, AnalysisErrorType.STATIC_WARNING, + correction: _correctionMessage, ); + static const _correctionMessage = + "Either set this prop, or mix it into the enclosing component's props and forward it."; + static DiagnosticCode _codeForRequiredness(PropRequiredness requiredness) { switch (requiredness) { case PropRequiredness.late: @@ -168,8 +174,9 @@ class MissingRequiredPropDiagnostic extends ComponentUsageDiagnosticContributor final debugHelper = AnalyzerDebugHelper(result, collector, enabled: _cachedIsDebugHelperEnabled(result)); // A flag to help verify during debugging/testing whether propsSetByFactory was computed. var hasPropsSetByFactoryBeenComputed = false; + final debugSuppressedRequiredPropsDueToForwarding = {}; - // Use a late variable to compute this only when we need to. + // Use late variables to compute these only when we need to. late final propsSetByFactory = () { hasPropsSetByFactoryBeenComputed = true; @@ -181,8 +188,8 @@ class MissingRequiredPropDiagnostic extends ComponentUsageDiagnosticContributor return _cachedGetPropsSetByFactory(factoryElement); }(); - - final presentPropNames = + late final forwardedProps = computeForwardedProps(usage); + late final presentPropNames = usage.cascadedProps.where((prop) => !prop.isPrefixed).map((prop) => prop.name.name).toSet(); for (final name in requiredPropInfo.requiredPropNames) { @@ -198,7 +205,15 @@ class MissingRequiredPropDiagnostic extends ComponentUsageDiagnosticContributor continue; } - // TODO(FED-2034) don't warn when we know required props are being forwarded + final sourcePropsClass = field.enclosingElement; + if (sourcePropsClass is InterfaceElement) { + if (forwardedProps != null && forwardedProps.definitelyForwardsPropsFrom(sourcePropsClass)) { + if (debugHelper.enabled) { + debugSuppressedRequiredPropsDueToForwarding.add(field); + } + continue; + } + } // Only access propsSetByFactory when we hit missing required props to avoid computing it unnecessarily. if (propsSetByFactory?.contains(name) ?? false) { @@ -221,6 +236,20 @@ class MissingRequiredPropDiagnostic extends ComponentUsageDiagnosticContributor } } + if (forwardedProps != null) { + debugHelper.log(() { + var message = StringBuffer()..writeln(forwardedProps); + if (debugSuppressedRequiredPropsDueToForwarding.isNotEmpty) { + final propsNamesByClassName = >{}; + for (final field in debugSuppressedRequiredPropsDueToForwarding) { + propsNamesByClassName.putIfAbsent(field.enclosingElement.name, () => {}).add(field.name); + } + message.write('Required props set only via forwarding: ${prettyPrint(propsNamesByClassName)}'); + } else {} + return message.toString(); + }, () => result.locationFor(forwardedProps.debugSourceNode)); + } + // Include debug info for each invocation ahout all the props and their requirednesses. debugHelper.log(() { final propNamesByRequirednessName = >{}; diff --git a/tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarded_props.dart b/tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarded_props.dart new file mode 100644 index 000000000..2e980ac1f --- /dev/null +++ b/tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarded_props.dart @@ -0,0 +1,128 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; +import 'package:over_react_analyzer_plugin/src/util/ast_util.dart'; +import 'package:over_react_analyzer_plugin/src/util/is_props_from_render.dart'; +import 'package:over_react_analyzer_plugin/src/util/prop_forwarding/parse_forwarding_config.dart'; +import 'package:over_react_analyzer_plugin/src/util/util.dart'; + +import 'forwarding_config.dart'; +import 'util.dart'; + +/// A representation of props forwarded to a component usage. +class ForwardedProps { + /// The props class that props are being forwarded from. + /// + /// For example, the type of `props` in `..addUnconsumedProps(props, ...)`. + final InterfaceElement propsClassBeingForwarded; + + /// The configuration of which props to forward, or null if it could not be resolved. + final PropForwardingConfig? forwardingConfig; + + /// A node that represents the addition of forwarded props, for use in debug infos only. + final AstNode debugSourceNode; + + ForwardedProps(this.propsClassBeingForwarded, this.forwardingConfig, this.debugSourceNode); + + /// Returns whether these forwarded props definitely include props from [propsClass], or false + /// if forwarded props could not be resolved. + /// + /// This is true only when all of the following conditions are met: + /// - [propsClassBeingForwarded] inherits from [propsClass] (i.e., is or mixes in those props) + /// - [propsClass] is not excluded by [forwardingConfig] + bool definitelyForwardsPropsFrom(InterfaceElement propsClass) { + final forwardingConfig = this.forwardingConfig; + if (forwardingConfig == null) return false; + + // Handle legacy classes being passed in. + if (propsClass.name.startsWith(r'_$')) { + // Look up the companion and use that instead, since that's what will be referenced in the forwarding config. + // E.g., for `_$FooProps`, find `FooProps`, since consumers will be using `FooProps` when setting up prop forwarding. + final companion = propsClassBeingForwarded.thisAndAllSuperInterfaces + .whereType() + .singleWhereOrNull((c) => c.supertype?.element == propsClass && '_\$${c.name}' == propsClass.name); + // If we can't find the companion, return false, since it won't show up in the forwarding config. + if (companion == null) return false; + propsClass = companion; + } + + return !forwardingConfig.excludesProps(propsClass) && + propsClassBeingForwarded.thisAndAllSuperInterfaces.contains(propsClass); + } + + @override + String toString() => 'Forwards props from ${propsClassBeingForwarded.name}: ${forwardingConfig ?? '(unresolved)'}'; +} + +extension on InterfaceElement { + /// This interface and all its superinterfaces. + /// + /// Computed lazily, since [allSupertypes] is expensive. + Iterable get thisAndAllSuperInterfaces sync* { + yield this; + yield* allSupertypes.map((s) => s.element); + } +} + +/// Computes and returns forwarded props for a given component [usage], or `null` if the usage does not receive any +/// forwarded props. +ForwardedProps? computeForwardedProps(FluentComponentUsage usage) { + // Lazy variables for potentially expensive values that may get used in multiple loop iterations. + late final enclosingComponentPropsClass = + getTypeOfPropsInEnclosingInterface(usage.node)?.typeOrBound.element.tryCast(); + + for (final invocation in usage.cascadedMethodInvocations) { + final methodName = invocation.methodName.name; + final arg = invocation.node.argumentList.arguments.firstOrNull; + + if (methodName == 'addProps' || methodName == 'modifyProps') { + // If props are conditionally forwarded, don't count them. + final hasConditionArg = invocation.node.argumentList.arguments.length > 1; + if (hasConditionArg) continue; + } + + final isAddAllOrAddProps = methodName == 'addProps' || methodName == 'addAll'; + + // ..addProps(props) + if (isAddAllOrAddProps && arg != null && isPropsFromRender(arg)) { + final propsType = arg.staticType?.typeOrBound.tryCast()?.element; + if (propsType != null) { + return ForwardedProps(propsType, PropForwardingConfig.all(), invocation.node); + } + } else if ( + // ..addProps(props.getPropsToForward(...)) + (isAddAllOrAddProps && arg is MethodInvocation && arg.methodName.name == 'getPropsToForward') || + // ..modifyProps(props.addPropsToForward(...)) + (methodName == 'modifyProps' && arg is MethodInvocation && arg.methodName.name == 'addPropsToForward')) { + final realTarget = arg.realTarget; + if (realTarget != null && isPropsFromRender(realTarget)) { + final propsType = realTarget.staticType?.typeOrBound.tryCast()?.element; + if (propsType != null) { + return ForwardedProps(propsType, parsePropsToForwardMethodArgs(arg.argumentList, propsType), invocation.node); + } + } + } else if ( + // ..addProps(copyUnconsumedProps()) + (isAddAllOrAddProps && arg is MethodInvocation && arg.methodName.name == 'copyUnconsumedProps') || + // ..modifyProps(addUnconsumedProps) + (methodName == 'modifyProps' && arg is Identifier && arg.name == 'addUnconsumedProps')) { + if (enclosingComponentPropsClass != null) { + return ForwardedProps( + enclosingComponentPropsClass, parseEnclosingClassComponentConsumedProps(usage.node), invocation.node); + } + } else if ( + // ..addUnconsumedProps(props, consumedProps) + methodName == 'addUnconsumedProps') { + final consumedPropsArg = invocation.node.argumentList.arguments.elementAtOrNull(1); + if (arg != null && consumedPropsArg != null && isPropsFromRender(arg)) { + final propsType = arg.staticType?.typeOrBound.tryCast()?.element; + if (propsType != null) { + return ForwardedProps(propsType, parseConsumedProps(consumedPropsArg), invocation.node); + } + } + } + } + + return null; +} diff --git a/tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarding_config.dart b/tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarding_config.dart new file mode 100644 index 000000000..a556d9594 --- /dev/null +++ b/tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarding_config.dart @@ -0,0 +1,46 @@ +import 'package:analyzer/dart/element/element.dart'; + +/// A representation of an over_react consumer's configuration of which props classes to +/// include or exclude when forwarding props. +abstract class PropForwardingConfig { + const PropForwardingConfig(); + + const factory PropForwardingConfig.all() = _PropForwardingConfig$AllExceptFor; + + const factory PropForwardingConfig.allExceptFor(Set onlyProps) = _PropForwardingConfig$AllExceptFor; + + const factory PropForwardingConfig.only(Set excludedProps) = _PropForwardingConfig$Only; + + /// Whether this configuration might exclude props declared in the props class [e] when forwarding. + bool excludesProps(InterfaceElement e); + + String get debugDescription; + + @override + toString() => '$debugDescription'; +} + +class _PropForwardingConfig$Only extends PropForwardingConfig { + final Set _onlyProps; + + const _PropForwardingConfig$Only(this._onlyProps); + + @override + bool excludesProps(InterfaceElement e) => !_onlyProps.contains(e); + + @override + String get debugDescription => 'only props from ${_onlyProps.map((e) => e.name).toSet()}'; +} + +class _PropForwardingConfig$AllExceptFor extends PropForwardingConfig { + final Set _excludedProps; + + const _PropForwardingConfig$AllExceptFor([this._excludedProps = const {}]); + + @override + bool excludesProps(InterfaceElement e) => _excludedProps.contains(e); + + @override + String get debugDescription => + _excludedProps.isEmpty ? 'all props' : 'all except props from ${_excludedProps.map((e) => e.name).toSet()}'; +} diff --git a/tools/analyzer_plugin/lib/src/util/prop_forwarding/parse_forwarding_config.dart b/tools/analyzer_plugin/lib/src/util/prop_forwarding/parse_forwarding_config.dart new file mode 100644 index 000000000..c323f20e7 --- /dev/null +++ b/tools/analyzer_plugin/lib/src/util/prop_forwarding/parse_forwarding_config.dart @@ -0,0 +1,171 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:over_react_analyzer_plugin/src/util/ast_util.dart'; +import 'package:over_react_analyzer_plugin/src/util/util.dart'; +import 'package:over_react_analyzer_plugin/src/util/react_types.dart'; + +import 'forwarding_config.dart'; +import 'util.dart'; + +/// Returns a forwarding config parsed/resolved from arguments to `.getPropsToForward` or `addPropsToForward`. +/// +/// Returns null if the forwarded props could not be resolved. +/// +/// Parses: +/// +/// ..addProps(props.getPropsToForward()) +/// ..addProps(props.getPropsToForward(exclude: {...})) +/// ..modifyProps(props.addPropsToForward(exclude: {...})) +PropForwardingConfig? parsePropsToForwardMethodArgs(ArgumentList argumentList, InterfaceElement propsType) { + final excludeArg = argumentList.arguments + .whereType() + .firstWhereOrNull((a) => a.name.label.name == 'exclude') + ?.expression; + + // With no exclude, the default is the props in the current props type + if (excludeArg == null || excludeArg is NullLiteral) { + return PropForwardingConfig.allExceptFor({propsType}); + } + + // Not a literal we can statically analyze + if (excludeArg is! SetOrMapLiteral || !excludeArg.isSet) { + return null; + } + + final excluded = _resolveInterfaceReferences(excludeArg.elements)?.toSet(); + if (excluded == null) return null; + return PropForwardingConfig.allExceptFor(excluded); +} + +/// Returns a forwarding config parsed/resolved from a [consumedProps] expression representing the set of +/// props mixins to exclude when forwarding, which is used by `UiProps.addUnconsumedProps` and also UiComponent's +/// `consumedProps` getter. +/// +/// Returns null if the forwarded props could not be resolved. +/// +/// Handles various different syntaxes (see comments in this method for examples). +PropForwardingConfig? parseConsumedProps(Expression consumedProps) { + // Look up value of consumedProps stored into variables, which is very common in usage: + // + // final consumedProps = props.staticMeta.forMixins({...}); + if (consumedProps is Identifier) { + final staticElement = consumedProps.staticElement; + if (staticElement == null) return null; + + final variableValue = lookUpVariable(staticElement, consumedProps.root)?.initializer; + // Don't recurse for Identifiers, to prevent potential infinite loops; + // this case should not be common so we're safe to ignore it. + if (variableValue != null && variableValue is! Identifier) { + return parseConsumedProps(variableValue); + } + + return null; + } + + // We could validate the target of this call, but these methods are specifically-named enough that false positives + // are unlikely, and validating the target would require a bit of extra logic. + // + // This invocation will usually look like: + // - `propsMeta.forMixins` - class component `propsMeta` getter + // - `props.staticMeta.forMixins` - props class `staticMeta` getter: + if (consumedProps is MethodInvocation) { + if (consumedProps.methodName.name == 'forMixins') { + final arg = consumedProps.argumentList.arguments.whereType().firstOrNull; + if (arg is SetOrMapLiteral) { + final mixins = _resolveInterfaceReferences(arg.elements)?.toSet(); + if (mixins != null) { + return PropForwardingConfig.allExceptFor(mixins); + } + } + return null; + } else if (consumedProps.methodName.name == 'allExceptForMixins') { + final arg = consumedProps.argumentList.arguments.whereType().firstOrNull; + if (arg is SetOrMapLiteral) { + final mixins = _resolveInterfaceReferences(arg.elements)?.toSet(); + if (mixins != null) { + return PropForwardingConfig.only(mixins); + } + } + return null; + } + } + + final consumedPropElements = + consumedProps.tryCast()?.elements ?? consumedProps.tryCast()?.elements; + if (consumedPropElements != null) { + // get consumedProps => const [LightboxProps.meta] + final excluded = {}; + for (final consumedPropElement in consumedPropElements) { + if (consumedPropElement is Expression) { + final result = getSimpleTargetAndPropertyName(consumedPropElement, allowMethodInvocation: false); + if (result != null && result.item2.name == 'meta') { + final propsClass = result.item1.staticElement.tryCast(); + if (propsClass != null) { + excluded.add(propsClass); + continue; + } + } + } + // Short-circuit if we encounter any other cases. + return null; + } + return PropForwardingConfig.allExceptFor(excluded); + } + + return null; +} + +/// Returns a forwarding config parsed/resolved from the `UiComponent.consumedProps` override in the component class +/// that encloses [node]. +/// +/// Returns null if the forwarded props could not be resolved. +PropForwardingConfig? parseEnclosingClassComponentConsumedProps(AstNode node) { + final enclosingComponentPropsClass = + getTypeOfPropsInEnclosingInterface(node)?.typeOrBound.element.tryCast(); + if (enclosingComponentPropsClass == null) return null; + + final enclosingClass = node.thisOrAncestorOfType(); + final consumedPropsGetter = + enclosingClass?.members.whereType().firstWhereOrNull((m) => m.name.lexeme == 'consumedProps'); + + // The default is only props declared in enclosingComponentPropsClass for legacy boilerplate, and empty for non-legacy boilerplate. + if (consumedPropsGetter == null) { + final isLegacy = enclosingClass?.declaredElement?.isLegacyComponentClass ?? false; + return isLegacy ? PropForwardingConfig.allExceptFor({enclosingComponentPropsClass}) : PropForwardingConfig.only({}); + } + + final consumedProps = consumedPropsGetter.body.returnExpressions.singleOrNull; + if (consumedProps == null) return null; + + return parseConsumedProps(consumedProps); +} + +/// Resolves AST [elements] representing references to interface types to the elements they reference. +/// +/// Returns null if not all of the references could be resolved, or if if-elements or for-elements are used. +/// +/// For example, given either the AST Node: +/// - `{SomeClass, SomeMixin}` (a [SetOrMapLiteral]) +/// - `[SomeClass, SomeMixin]` (a [ListLiteral]) +/// +/// ...passing in these collection's `.elements` would yield a list containing the [ClassElement] representing `SomeClass` +/// and the [MixinElement] for SomeMixin. +List? _resolveInterfaceReferences(List elements) { + final interfaces = []; + for (final setElement in elements) { + // Some other element or expression we can't statically analyze. + // It seems like this should be a TypeLiteral, + // but it seems to be a SimpleIdentifier. + // Handle both cases in case it's sometimes one and sometimes the other. + InterfaceElement? interface; + if (setElement is Identifier) { + interface = setElement.staticElement.tryCast(); + } else if (setElement is TypeLiteral) { + interface = setElement.type.element.tryCast(); + } + if (interface == null) return null; + + interfaces.add(interface); + } + return interfaces; +} diff --git a/tools/analyzer_plugin/lib/src/util/prop_forwarding/util.dart b/tools/analyzer_plugin/lib/src/util/prop_forwarding/util.dart new file mode 100644 index 000000000..b54cd5306 --- /dev/null +++ b/tools/analyzer_plugin/lib/src/util/prop_forwarding/util.dart @@ -0,0 +1,23 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:over_react_analyzer_plugin/src/util/ast_util.dart'; +import 'package:over_react_analyzer_plugin/src/util/util.dart'; + +DartType? getTypeOfPropsInEnclosingInterface(AstNode node) { + final enclosingInterface = _getEnclosingInterface(node); + if (enclosingInterface == null) return null; + + return enclosingInterface.thisType.lookUpGetter2('props', enclosingInterface.library)?.returnType; +} + +InterfaceElement? _getEnclosingInterface(AstNode node) { + for (final ancestor in node.ancestors) { + if (ancestor is ClassDeclaration) return ancestor.declaredElement; + if (ancestor is MixinDeclaration) return ancestor.declaredElement; + if (ancestor is ExtensionDeclaration) { + return ancestor.declaredElement?.extendedType.typeOrBound.tryCast()?.element; + } + } + return null; +} diff --git a/tools/analyzer_plugin/lib/src/util/react_types.dart b/tools/analyzer_plugin/lib/src/util/react_types.dart index 0cc46b3c5..fe72a9c72 100644 --- a/tools/analyzer_plugin/lib/src/util/react_types.dart +++ b/tools/analyzer_plugin/lib/src/util/react_types.dart @@ -4,20 +4,35 @@ import 'ast_util.dart'; extension ReactTypes$DartType on DartType { bool get isComponentClass => typeOrBound.element?.isComponentClass ?? false; + + bool get isLegacyComponentClass => typeOrBound.element?.isLegacyComponentClass ?? false; + bool get isReactElement => typeOrBound.element?.isReactElement ?? false; + bool get isPropsClass => typeOrBound.element?.isPropsClass ?? false; + bool get isStateHook => typeOrBound.element?.isStateHook ?? false; + bool get isReducerHook => typeOrBound.element?.isReducerHook ?? false; + bool get isTransitionHook => typeOrBound.element?.isTransitionHook ?? false; } extension ReactTypes$Element on Element { bool get isComponentClass => isOrIsSubtypeOfElementFromPackage('Component', 'react'); + + bool get isLegacyComponentClass => isComponentClass && !isOrIsSubtypeOfElementFromPackage('Component2', 'react'); + bool get isReactElement => isOrIsSubtypeOfElementFromPackage('ReactElement', 'react'); + bool get isPropsClass => isOrIsSubtypeOfElementFromPackage('UiProps', 'over_react'); + bool get isStateHook => isOrIsSubtypeOfElementFromPackage('StateHook', 'react'); + bool get isReducerHook => isOrIsSubtypeOfElementFromPackage('ReducerHook', 'react'); + bool get isTransitionHook => false; + // TODO uncomment one useTransition/TransitionHook is implemented // bool get isTransitionHook => isOrIsSubtypeOfElementFromPackage('TransitionHook', 'react'); diff --git a/tools/analyzer_plugin/playground/web/missing_required_props.dart b/tools/analyzer_plugin/playground/web/missing_required_props.dart index 10367e564..eb436daae 100644 --- a/tools/analyzer_plugin/playground/web/missing_required_props.dart +++ b/tools/analyzer_plugin/playground/web/missing_required_props.dart @@ -106,3 +106,22 @@ main() { // Make sure prefixed props aren't mistaken for the missing required prop. (RequiredWithSameNameAsPrefixed()..dom.hidden = true)(); } + +class WrapsInheritsLateRequiredProps = UiProps with WithLateRequiredProps, InheritsLateRequiredPropsMixin; + +UiFactory WrapsInheritsLateRequired = uiFunction( + (props) { + // Forwarding required props mixed into `props` counts as setting them, + // and this should not lint. + return (InheritsLateRequired() + ..addProps(props.getPropsToForward(exclude: { + // Try uncommenting these to see lints for props that don't get forwarded. + // WithLateRequiredProps, + // InheritsLateRequiredPropsMixin, + })) + )(); + // de bug:over_react_required_props + // ^ Also, try deleting this space to enable prop forwarding debug infos on lines where props are forwarded + }, + _$WrapsInheritsLateRequiredConfig, // ignore: undefined_identifier +); diff --git a/tools/analyzer_plugin/test/integration/diagnostics/missing_required_prop_test.dart b/tools/analyzer_plugin/test/integration/diagnostics/missing_required_prop_test.dart index 05cfa3df8..50af8df57 100644 --- a/tools/analyzer_plugin/test/integration/diagnostics/missing_required_prop_test.dart +++ b/tools/analyzer_plugin/test/integration/diagnostics/missing_required_prop_test.dart @@ -13,6 +13,7 @@ void main() { defineReflectiveTests(MissingRequiredPropTest_NoErrors); defineReflectiveTests(MissingRequiredPropTest_MissingLateRequired); defineReflectiveTests(MissingRequiredPropTest_MissingAnnotationRequired); + defineReflectiveTests(MissingRequiredPropTest_Forwarding); }); } @@ -360,3 +361,132 @@ over_react: ''')); } } + +@reflectiveTest +class MissingRequiredPropTest_Forwarding extends MissingRequiredPropTest { + @override + get errorUnderTest => MissingRequiredPropDiagnostic.lateRequiredCode; + + @override + get fixKindUnderTest => MissingRequiredPropDiagnostic.fixKind; + + // More variations on prop forwarding are covered in prop_forwarding_test.dart; + // these tests mainly verify the logic in the diagnostic and the end-to-end behavior + + Future test_noErrorsWhenForwarded() async { + await expectNoErrors(newSourceWithPrefix(/*language=dart*/ r''' + class MultipleRequiredMixinsProps = UiProps with WithLateRequiredProps, InheritsLateRequiredPropsMixin; + UiFactory MultipleRequiredMixins = uiFunction((props) { + return (InheritsLateRequired() + ..modifyProps(props.addPropsToForward(exclude: {})) + )(); + }, _$MultipleRequiredMixinsConfig); + ''')); + } + + Future test_errorsWhenNotForwarded() async { + final source = newSourceWithPrefix(/*language=dart*/ r''' + class MultipleRequiredMixinsProps = UiProps with WithLateRequiredProps, InheritsLateRequiredPropsMixin; + UiFactory MultipleRequiredMixins = uiFunction((props) { + return (InheritsLateRequired() + ..modifyProps(props.addPropsToForward(exclude: {WithLateRequiredProps, InheritsLateRequiredPropsMixin})) + )(); + }, _$MultipleRequiredMixinsConfig); + '''); + final selection = createSelection(source, '#InheritsLateRequired()#'); + final allErrors = await getAllErrors(source); + expect( + allErrors, + unorderedEquals([ + isAnErrorUnderTest(locatedAt: selection).havingMessage(contains("'required1' from 'WithLateRequiredProps'")), + isAnErrorUnderTest(locatedAt: selection).havingMessage(contains("'required2' from 'WithLateRequiredProps'")), + isAnErrorUnderTest(locatedAt: selection) + .havingMessage(contains("'requiredInSubclass' from 'InheritsLateRequiredPropsMixin'")), + ])); + } + + Future test_errorsWhenOnlySomeForwarded() async { + final source = newSourceWithPrefix(/*language=dart*/ r''' + class MultipleRequiredMixinsProps = UiProps with WithLateRequiredProps, InheritsLateRequiredPropsMixin; + UiFactory MultipleRequiredMixins = uiFunction((props) { + return (InheritsLateRequired() + ..modifyProps(props.addPropsToForward(exclude: {InheritsLateRequiredPropsMixin})) + )(); + }, _$MultipleRequiredMixinsConfig); + '''); + final selection = createSelection(source, '#InheritsLateRequired()#'); + final allErrors = await getAllErrors(source); + expect( + allErrors, + unorderedEquals([ + isAnErrorUnderTest(locatedAt: selection) + .havingMessage(contains("'requiredInSubclass' from 'InheritsLateRequiredPropsMixin'")), + ])); + } + + static const legacyPrefix = /*language=dart*/ r''' + @Factory() + UiFactory LegacyBase = castUiFactory(_$LegacyBase); + @Props() + class _$LegacyBaseProps extends UiProps { + late String required_legacyBaseProps; + } + class LegacyBaseProps extends _$LegacyBaseProps with + // ignore: undefined_identifier, mixin_of_non_class + _$LegacyBasePropsAccessorsMixin { + // ignore: undefined_identifier, const_initialized_with_non_constant_value, invalid_assignment + static const PropsMeta meta = _$metaForLegacyBaseProps; + } + @Component() + class LegacyBaseComponent extends UiComponent { + @override + render() => null; + } + + @Factory() + UiFactory Legacy = castUiFactory(_$Legacy); + @Props() + class _$LegacyProps extends LegacyBaseProps {} + class LegacyProps extends _$LegacyProps with + // ignore: undefined_identifier, mixin_of_non_class + _$LegacyPropsAccessorsMixin { + // ignore: undefined_identifier, const_initialized_with_non_constant_value, invalid_assignment + static const PropsMeta meta = _$metaForLegacyProps; + } + '''; + + Future test_legacyErrorsWhenNotForwarded() async { + final source = newSourceWithPrefix(legacyPrefix + /*language=dart*/ + r''' + @Component() + class LegacyComponent extends UiComponent { + get consumedProps => const [LegacyProps.meta, LegacyBaseProps.meta]; + @override + render() => (LegacyBase()..addProps(copyUnconsumedProps()))(); + } + '''); + final selection = createSelection(source, '#LegacyBase()#'); + final allErrors = await getAllErrors(source); + expect( + allErrors, + unorderedEquals([ + isAnErrorUnderTest(locatedAt: selection) + .havingMessage(contains(r"'required_legacyBaseProps' from '_$LegacyBaseProps'")), + ])); + } + + Future test_legacyNoErrorsWhenForwarded() async { + // This test case verifies that looking up props class works even when using legacy component syntax, + // specifically props declared in legacy props classes that are different than their public types + // (e.g., _$LegacyBaseProps vs LegacyProps). + await expectNoErrors(newSourceWithPrefix(legacyPrefix + /*language=dart*/ + r''' + @Component() + class LegacyComponent extends UiComponent { + get consumedProps => const [LegacyProps.meta]; + @override + render() => (LegacyBase()..addProps(copyUnconsumedProps()))(); + } + ''')); + } +} diff --git a/tools/analyzer_plugin/test/unit/util/prop_forwarding_test.dart b/tools/analyzer_plugin/test/unit/util/prop_forwarding_test.dart new file mode 100644 index 000000000..b697a5cd7 --- /dev/null +++ b/tools/analyzer_plugin/test/unit/util/prop_forwarding_test.dart @@ -0,0 +1,1009 @@ +// Copyright 2024 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:over_react_analyzer_plugin/src/diagnostic_contributor.dart'; +import 'package:over_react_analyzer_plugin/src/util/prop_forwarding/forwarded_props.dart'; +import 'package:test/test.dart'; + +import '../../util/shared_analysis_context.dart'; +import 'prop_declaration/util.dart'; + +void main() { + group('prop_forwarding:', () { + final sharedContext = SharedAnalysisContext.overReact; + + setUpAll(sharedContext.warmUpAnalysis); + + group('computeForwardedProps correctly computes forwarded props for various cases:', () { + const sharedSourcePrefix = /*language=dart*/ r''' + // @dart=2.12 + import 'package:over_react/over_react.dart'; + + part '{{PART_PATH}}'; + + // [1] Create static meta variables that we only support for legacy props. + // We want to test referencing them, and we'll do so with these mixins + // to be able to reuse this test setup. + // [2] We can skip most all the analyzer ignore comments you'd usually need for this boilerplate, + // since these tests generate code first before analyzing. + + mixin AProps on UiProps { + static const meta = $AProps.meta; // [1] + + String? a; + } + mixin BProps on UiProps { + static const meta = $BProps.meta; // [1] + + String? b; + } + mixin CProps on UiProps { + static const meta = $CProps.meta; // [1] + + String? c; + } + + class HasABCProps = UiProps with AProps, BProps, CProps; + + mixin UnrelatedProps on UiProps {} + + late UiFactory NotCare; + + // [2] + @AbstractProps() + abstract class _$LegacyUnrelatedProps extends UiProps {} + abstract class LegacyUnrelatedProps extends _$LegacyUnrelatedProps with _$LegacyUnrelatedPropsAccessorsMixin { + static const PropsMeta meta = _$metaForLegacyUnrelatedProps; + } + + late bool condition; + '''; + + Future sharedResolveSource(String source) => + resolveFileAndGeneratedPart(sharedContext, '$sharedSourcePrefix\n$source'); + + group('adding `props` without filtering:', () { + for (final methodName in ['addAll', 'addProps']) { + group('via $methodName:', () { + test('', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..METHOD_NAME(props) + )(); + }, _$HasABCConfig); + ''' + .replaceAll('METHOD_NAME', methodName)); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + group('unless props are not from render, and gracefully handles these cases:', () { + test('typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + late HasABCProps notPropsFromRender; + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..METHOD_NAME(notPropsFromRender) + )(); + }, _$HasABCConfig); + ''' + .replaceAll('METHOD_NAME', methodName)); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + + test('not typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..METHOD_NAME({}) + )(); + }, _$HasABCConfig); + ''' + .replaceAll('METHOD_NAME', methodName)); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + }); + } + + test('unless props are conditionally added', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addProps(props, condition) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + + group('UiProps.addUnconsumedProps:', () { + group('..addUnconsumedProps(props, ...)', () { + test('using .forMixins', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addUnconsumedProps(props, props.staticMeta.forMixins({AProps, BProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('using .allExceptForMixins', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addUnconsumedProps(props, props.staticMeta.allExceptForMixins({BProps, CProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + }); + + test('..addUnconsumedProps(props, consumedPropsVariable)', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + final consumedProps = props.staticMeta.forMixins({AProps, BProps}); + return (NotCare() + ..addUnconsumedProps(props, consumedProps) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + group('unless props are not from render, and gracefully handles these cases:', () { + test('typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + late HasABCProps notPropsFromRender; + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addUnconsumedProps(notPropsFromRender, props.staticMeta.forMixins({AProps, BProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + + test('not typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addUnconsumedProps({}, props.staticMeta.forMixins({AProps, BProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + }); + + group('PropsToForward.getPropsToForward:', () { + for (final methodName in ['addAll', 'addProps']) { + group('adding via $methodName:', () { + group('props.getPropsToForward(exclude: {...})', () { + test('', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..METHOD_NAME(props.getPropsToForward(exclude: {AProps, BProps})) + )(); + }, _$HasABCConfig); + ''' + .replaceAll('METHOD_NAME', methodName)); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage); + expect(forwardedProps, isNotNull); + expect(forwardedProps!.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + group('unless props are not from render, and gracefully handles these cases:', () { + test('typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + late HasABCProps notPropsFromRender; + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..METHOD_NAME(notPropsFromRender.getPropsToForward(exclude: {AProps, BProps})) + )(); + }, _$HasABCConfig); + ''' + .replaceAll('METHOD_NAME', methodName)); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + + test('not typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + late UiProps notPropsFromRender; + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..METHOD_NAME(notPropsFromRender.getPropsToForward(exclude: {AProps, BProps})) + )(); + }, _$HasABCConfig); + ''' + .replaceAll('METHOD_NAME', methodName)); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + }); + + test('props.getPropsToForward() - inferred from generic', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory A = uiFunction((props) { + return (NotCare() + ..METHOD_NAME(props.getPropsToForward()) + )(); + }, _$AConfig); + + // For this test case, unlike others, we need a component that only has one props mixin; + // declare this unused HasABC so we don't get builder errors. + UiFactory HasABC = uiFunction((props) {}, _$HasABCConfig); + ''' + .replaceAll('METHOD_NAME', methodName)); + final usage = getAllComponentUsages(result.unit).single; + + final propsElement = result.lookUpInterface('AProps'); + + final forwardedProps = computeForwardedProps(usage); + expect(forwardedProps!.propsClassBeingForwarded, propsElement); + expect(forwardedProps.definitelyForwardsPropsFrom(propsElement), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + }); + } + + test('unless props are conditionally added', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addProps(props.getPropsToForward(exclude: {AProps, BProps}), condition) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + + group('PropsToForward.addPropsToForward:', () { + group('..modifyProps(props.addPropsToForward(exclude: {...}))', () { + test('', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..modifyProps(props.addPropsToForward(exclude: {AProps, BProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage); + expect(forwardedProps, isNotNull); + expect(forwardedProps!.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + group('unless props are not from render, and gracefully handles these cases:', () { + test('typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + late HasABCProps notPropsFromRender; + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..modifyProps(notPropsFromRender.addPropsToForward(exclude: {AProps, BProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + + test('not typed as props', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + late UiProps notPropsFromRender; + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addProps(notPropsFromRender.getPropsToForward(exclude: {AProps, BProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + }); + + test('..modifyProps(props.addPropsToForward()) - inferred from generic', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory A = uiFunction((props) { + return (NotCare() + ..modifyProps(props.addPropsToForward()) + )(); + }, _$AConfig); + + // For this test case, unlike others, we need a component that only has one props mixin; + // declare this unused HasABC so we don't get builder errors. + UiFactory HasABC = uiFunction((props) {}, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final propsElement = result.lookUpInterface('AProps'); + + final forwardedProps = computeForwardedProps(usage); + expect(forwardedProps!.propsClassBeingForwarded, propsElement); + expect(forwardedProps.definitelyForwardsPropsFrom(propsElement), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('unless props are conditionally added', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..modifyProps(props.addPropsToForward(exclude: {AProps, BProps}), condition) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + + group('class component methods:', () { + group('when consumedProps uses', () { + group('static meta variables (legacy boilerplate only)', () { + test('in a list', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => [AProps.meta, BProps.meta]; + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('in a set', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => {AProps.meta, BProps.meta}; + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + }); + + test('propsMeta.forMixins', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => propsMeta.forMixins({AProps, BProps}); + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('propsMeta.allExceptForMixins', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => propsMeta.allExceptForMixins({BProps, CProps}); + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('props.staticMeta.forMixins', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => props.staticMeta.forMixins({AProps, BProps}); + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('props.staticMeta.allExceptForMixins', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => props.staticMeta.allExceptForMixins({BProps, CProps}); + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + group('empty list', () { + test('', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => []; + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('empty list (const)', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => const []; + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + }); + + test('empty set', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => {}; + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + }); + + test('..addProps(copyUnconsumedProps())', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => propsMeta.forMixins({AProps, BProps}); + @override render() { + return (NotCare() + ..addProps(copyUnconsumedProps()) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('..addAll(copyUnconsumedProps())', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => propsMeta.forMixins({AProps, BProps}); + @override render() { + return (NotCare() + ..addAll(copyUnconsumedProps()) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + group('..modifyProps(addUnconsumedProps)', () { + test('', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => propsMeta.forMixins({AProps, BProps}); + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('unless props are conditionally added', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps => propsMeta.forMixins({AProps, BProps}); + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps, condition) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + + group('when there is no consumedProps override', () { + test('UiComponent', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + @Factory() + UiFactory LegacyHasABC = castUiFactory(_$LegacyHasABC); + + // [2] + @Props() + class _$LegacyHasABCProps extends UiProps with + AProps, $AProps, BProps, $BProps, CProps, $CProps {} + + class LegacyHasABCProps extends _$LegacyHasABCProps with _$LegacyHasABCPropsAccessorsMixin { + static const PropsMeta meta = _$metaForLegacyHasABCProps; + } + + @Component() + class LegacyHasABCComponent extends UiComponent { + @override render() { + return (NotCare() + ..addProps(copyUnconsumedProps()) + )(); + } + } + + // For this test case, unlike others, we need a legacy boilerplate declaration; + // declare this unused HasABC so we don't get builder errors. + UiFactory HasABC = uiFunction((props) {}, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('LegacyHasABCProps')); + // For UiComponent, only props declared in the concrete props class are considered consumed by default; + // props declared in other mixins are not included. + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('LegacyHasABCProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('UiComponent2', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = castUiFactory(_$HasABC); + class HasABCComponent extends UiComponent2 { + @override render() { + return (NotCare() + ..modifyProps(addUnconsumedProps) + )(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + }); + }); + + test("when the props being forwarded are the same type as the component's props", () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory A = uiFunction((props) { + return (NotCare() + ..addProps(props.getPropsToForward(exclude: {})) + )(); + }, _$AConfig); + + // For this test case, unlike others, we need a component that only has one props mixin; + // declare this unused HasABC so we don't get builder errors. + UiFactory HasABC = uiFunction((props) {}, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final propsElement = result.lookUpInterface('AProps'); + final forwardedProps = computeForwardedProps(usage); + expect(forwardedProps!.propsClassBeingForwarded, propsElement); + expect(forwardedProps.definitelyForwardsPropsFrom(propsElement), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + }); + + test('normalizes legacy boilerplate to their companion classes', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + @Factory() + UiFactory Legacy = castUiFactory(_$Legacy); + + // [2] + @Props() + class _$LegacyProps extends UiProps {} + + class LegacyProps extends _$LegacyProps with _$LegacyPropsAccessorsMixin { + static const PropsMeta meta = _$metaForLegacyProps; + } + + @Component() + class LegacyComponent extends UiComponent { + get consumedProps => []; + @override render() { + return (NotCare() + ..addProps(copyUnconsumedProps()) + )(); + } + } + + // For this test case, unlike others, we need a legacy boilerplate declaration; + // declare this unused HasABC so we don't get builder errors. + UiFactory HasABC = uiFunction((props) {}, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + + final forwardedProps = computeForwardedProps(usage)!; + expect(forwardedProps.forwardingConfig, isNotNull, reason: 'forwarding config should be resolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('LegacyProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('LegacyProps')), isTrue); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface(r'_$LegacyProps')), isTrue, + reason: 'should normalize to companion'); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('LegacyUnrelatedProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface(r'_$LegacyUnrelatedProps')), isFalse, + reason: 'should return false when companion is not inherited by props being forwarded'); + }); + + group("other cases that shouldn't be picked up as forwarded props:", () { + test('addUnconsumedDomProps', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..addUnconsumedDomProps(props, props.staticMeta.forMixins({AProps, BProps})) + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + + test('no forwarded props, just setting other props unrelated method calls', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return (NotCare() + ..id = '' + ..addTestId('foo') + )(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + + test('no cascade', () async { + final result = await sharedResolveSource(/*language=dart*/ r''' + UiFactory HasABC = uiFunction((props) { + return NotCare()(); + }, _$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + expect(computeForwardedProps(usage), isNull); + }); + }); + + group('returns a PropsToForward with a null forwarding config when forwarded props could not be resolved:', () { + Future sharedTest(String usageCascadeSource, {String otherSource = ''}) async { + final result = await sharedResolveSource(''' + UiFactory HasABC = uiFunction((props) { + $otherSource + return (NotCare() + $usageCascadeSource + )(); + }, _\$HasABCConfig); + '''); + final usage = getAllComponentUsages(result.unit).single; + final forwardedProps = computeForwardedProps(usage); + expect(forwardedProps, isNotNull); + expect(forwardedProps!.forwardingConfig, isNull, reason: 'should be unresolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + } + + Future sharedClassConsumedPropsTest(String consumedPropsBodySource) async { + final result = await sharedResolveSource(''' + UiFactory HasABC = castUiFactory(_\$HasABC); + class HasABCComponent extends UiComponent2 { + @override get consumedProps $consumedPropsBodySource + @override render() { + return (NotCare()..modifyProps(addUnconsumedProps))(); + } + } + '''); + final usage = getAllComponentUsages(result.unit).single; + final forwardedProps = computeForwardedProps(usage); + expect(forwardedProps, isNotNull); + expect(forwardedProps!.forwardingConfig, isNull, reason: 'should be unresolved'); + expect(forwardedProps.propsClassBeingForwarded, result.lookUpInterface('HasABCProps')); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('AProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('BProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('CProps')), isFalse); + expect(forwardedProps.definitelyForwardsPropsFrom(result.lookUpInterface('UnrelatedProps')), isFalse); + } + + group('unresolved props mixin references in ', () { + const unresolvablePropsMixinsSet = '''{ + NotARealPropsMixin, // ignore: undefined_identifier + }'''; + + test('getPropsToForward', () => sharedTest(''' + ..addProps(props.getPropsToForward(exclude: $unresolvablePropsMixinsSet)) + ''')); + + test('addPropsToForward', () => sharedTest(''' + ..modifyProps(props.addPropsToForward(exclude: $unresolvablePropsMixinsSet)) + ''')); + + test('addUnconsumedProps', () => sharedTest(''' + ..addUnconsumedProps(props, props.staticMeta.forMixins($unresolvablePropsMixinsSet)) + ''')); + + test('a consumedProps variable', () => sharedTest(''' + ..addUnconsumedProps(props, consumedProps) + ''', otherSource: 'final consumedProps = props.staticMeta.forMixins($unresolvablePropsMixinsSet);')); + }); + + group('conditional props mixin references in ', () { + const conditionalPropsMixinsSet = '''{ + AProps, + if (condition) BProps, + }'''; + + test('getPropsToForward', () => sharedTest(''' + ..addProps(props.getPropsToForward(exclude: $conditionalPropsMixinsSet)) + ''')); + + test('addPropsToForward', () => sharedTest(''' + ..modifyProps(props.addPropsToForward(exclude: $conditionalPropsMixinsSet)) + ''')); + + test('addUnconsumedProps', () => sharedTest(''' + ..addUnconsumedProps(props, props.staticMeta.forMixins($conditionalPropsMixinsSet)) + ''')); + + test('a consumedProps variable', () => sharedTest(''' + ..addUnconsumedProps(props, consumedProps) + ''', otherSource: 'final consumedProps = props.staticMeta.forMixins($conditionalPropsMixinsSet);')); + }); + + group('consumedProps variable that', () { + test('is unresolvable', () => sharedTest(''' + ..addUnconsumedProps( + props, + unresolvedConsumedPropsVariable, // ignore: undefined_identifier + ) + ''')); + + test('is not initialized in its declaration', () => sharedTest(''' + ..addUnconsumedProps(props, consumedProps) + ''', otherSource: ''' + Iterable consumedProps; + consumedProps = props.staticMeta.forMixins({AProps, BProps}); + ''')); + }); + + group('consumedProps getter that ', () { + test('references props meta that cannot be resolved', () => sharedClassConsumedPropsTest(''' + => [ + NotARealPropsMixin.meta, // ignore: undefined_identifier + ]; + ''')); + + test('has more than one return statements', () => sharedClassConsumedPropsTest(''' + { + if (condition) return {}; + return {AProps.meta}; + } + ''')); + + test('references something other than `PropsClass.meta`', () => sharedClassConsumedPropsTest(''' + { + const meta = AProps.meta; + return [meta]; + } + ''')); + }); + }); + }); + }); +} + +extension on ResolvedUnitResult { + /// Extension form of [getInterfaceElement] to help make tests more concise. + InterfaceElement lookUpInterface(String name) => getInterfaceElement(this, name); +}