-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FED-2034: Analyzer plugin required props validation: check forwarded props #944
Merged
rmconsole2-wf
merged 43 commits into
master
from
FED-2034-required-props-lint-respect-forwarded
Sep 13, 2024
Merged
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
2232681
Detect forwarded props, don't lint for forwarded required props
greglittlefield-wf e7fbe54
Remove duplicate, outdated isPropsFromRender implementation
greglittlefield-wf 645c551
Add comments for cases
greglittlefield-wf dc02ee9
Implement case: `..addUnconsumedProps(props, ...)`
greglittlefield-wf 7b6fb97
Include props.staticMeta case when looking up forwarded props
greglittlefield-wf a38d70b
Add some tests for getForwardedProps
greglittlefield-wf 7404c80
Remove unused typeSystem args
greglittlefield-wf a427fcc
Add cases for forwarding all props, fix addAll case
greglittlefield-wf bf0130a
Clean up test cases
greglittlefield-wf 4c3cbca
Use clearer names in test cases
greglittlefield-wf b410651
Test multiple props in sets
greglittlefield-wf f04896b
Add, organize test cases
greglittlefield-wf 1e3f41e
Refactor PropsToForward implementation to be less dynamic
greglittlefield-wf 879e07d
Reorganize prop forwarding code into multiple files, improve naming
greglittlefield-wf cfb1b4c
Naming/comment cleanup
greglittlefield-wf cd7bc63
Add more test cases, mostly for class components
greglittlefield-wf d9a16d2
Handle legacy class components without consumedProps, add test
greglittlefield-wf 3aa6538
Add negative test cases
greglittlefield-wf 28c5d4d
DRY up addAll/addProps tests
greglittlefield-wf a918af7
Add static meta variable test cases, fix inverted boolean
greglittlefield-wf 42c577d
Clean up variables and debug code
greglittlefield-wf 7705bec
Add doc comment
greglittlefield-wf 6fa947e
Add some test cases for new prop forwarding logic in diagnostic
greglittlefield-wf bf87c7a
Add tests for legacy props edge case
greglittlefield-wf 61ee0ba
Add workaround for legacy props
greglittlefield-wf fecb9b1
Add companion normalization case, regression test for same type case
greglittlefield-wf 295a58e
Fix false negative when propClassBeingForwarded == propsClass
greglittlefield-wf 623fc3f
Add missing analyzer exclude
greglittlefield-wf 3250872
Remove TODO, move forwarding check to after cheaper short-circuit
greglittlefield-wf 98adca6
Add doc comments improve name
greglittlefield-wf 0c7ec4c
Reference prop forwarding in lint tooltip
greglittlefield-wf 53d7f3d
Replace unresolved case with null
greglittlefield-wf 21c0b5c
Clean up naming, add doc comments
greglittlefield-wf 6f890e1
Expect resolved forwarding configs in test cases
greglittlefield-wf 954841e
Update debug comment description
greglittlefield-wf 113418e
Remove unnecessary null check leftover from unresolved class removal
greglittlefield-wf 2410b56
Add unresolved forwarding config test cases
greglittlefield-wf 835ca92
Use more verbose test variable name since it no longer affects wrapping
greglittlefield-wf 9125142
Add required props forwarding example to playground
greglittlefield-wf d0aca29
Consolidate "all" forwarding config case
greglittlefield-wf 943032f
Clean up and improve prop forwarding debug infos, add demo
greglittlefield-wf 0aa15fd
Add test cases for conditional forwarding
greglittlefield-wf 5e37b83
Bail out when props are conditionally forwarded
greglittlefield-wf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarded_props.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ClassElement>() | ||
.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<InterfaceElement> 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<InterfaceElement>(); | ||
|
||
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<InterfaceType>()?.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<InterfaceType>()?.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<InterfaceType>()?.element; | ||
if (propsType != null) { | ||
return ForwardedProps(propsType, parseConsumedProps(consumedPropsArg), invocation.node); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return null; | ||
} |
46 changes: 46 additions & 0 deletions
46
tools/analyzer_plugin/lib/src/util/prop_forwarding/forwarding_config.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept going back and forth between combining this class into
|
||
const PropForwardingConfig(); | ||
|
||
const factory PropForwardingConfig.all() = _PropForwardingConfig$AllExceptFor; | ||
|
||
const factory PropForwardingConfig.allExceptFor(Set<InterfaceElement> onlyProps) = _PropForwardingConfig$AllExceptFor; | ||
|
||
const factory PropForwardingConfig.only(Set<InterfaceElement> 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<InterfaceElement> _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<InterfaceElement> _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()}'; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a null key here indicative of things like ubiquitive / un-namespaced props - or something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's just that technically
.name
is nullable here, sincefield.enclosingElement
is typed asElement
and notInterfaceElement
(which has a non-nullable.name
).So in practice, this should never be null