| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analyzer/analysis_rule/analysis_rule.dart'; |
| import 'package:analyzer/analysis_rule/rule_context.dart'; |
| import 'package:analyzer/analysis_rule/rule_state.dart'; |
| import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/source/source_range.dart'; |
| import 'package:analyzer/src/utilities/extensions/string.dart'; // ignore: implementation_imports |
| |
| import '../analyzer.dart'; |
| import '../diagnostic.dart' as diag; |
| |
| const _desc = r'Use super-initializer parameters where possible.'; |
| |
| /// Return a set containing the elements of all of the parameters that are |
| /// referenced in the constructor [body]. |
| Set<FormalParameterElement> _referencedParameters(FunctionBody? body) { |
| if (body == null) return const {}; |
| var collector = _ReferencedParameterCollector(); |
| body.accept(collector); |
| return collector.foundParameters; |
| } |
| |
| class UseSuperParameters extends MultiAnalysisRule { |
| new() |
| : super( |
| name: LintNames.use_super_parameters, |
| description: _desc, |
| state: const RuleState.experimental(), |
| ); |
| |
| @override |
| List<DiagnosticCode> get diagnosticCodes => [ |
| diag.useSuperParametersMultiple, |
| diag.useSuperParametersSingle, |
| ]; |
| |
| @override |
| void registerNodeProcessors( |
| RuleVisitorRegistry registry, |
| RuleContext context, |
| ) { |
| if (!context.isFeatureEnabled(Feature.super_parameters)) return; |
| |
| var visitor = _Visitor(this, context); |
| registry.addConstructorDeclaration(this, visitor); |
| registry.addPrimaryConstructorDeclaration(this, visitor); |
| } |
| } |
| |
| class _ReferencedParameterCollector extends RecursiveAstVisitor<void> { |
| final Set<FormalParameterElement> foundParameters = {}; |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| var element = node.element; |
| if (element is FormalParameterElement) { |
| foundParameters.add(element); |
| } |
| } |
| } |
| |
| class _Visitor extends SimpleAstVisitor<void> { |
| final RuleContext context; |
| final MultiAnalysisRule rule; |
| |
| new(this.rule, this.context); |
| |
| void check( |
| SourceRange errorRange, |
| SuperConstructorInvocation superInvocation, |
| FormalParameterList parameters, |
| FunctionBody? body, |
| ) { |
| var constructorElement = superInvocation.element; |
| if (constructorElement == null) return; |
| |
| // TODO(pq): consolidate logic shared w/ server |
| // (https://github.com/dart-lang/linter/issues/3263) |
| |
| var referencedParameters = _referencedParameters(body); |
| |
| var identifiers = _checkForConvertiblePositionalParams( |
| constructorElement, |
| superInvocation, |
| parameters.parameters, |
| referencedParameters, |
| ); |
| |
| // Bail if the constructor is in a bad state. |
| if (identifiers == null) return; |
| |
| for (var parameter in parameters.parameters) { |
| var parameterElement = parameter.declaredFragment?.element; |
| if (parameterElement == null) continue; |
| if (parameterElement is FieldFormalParameterElement) continue; |
| if (parameterElement.isNamed && |
| !referencedParameters.contains(parameterElement)) { |
| if (_checkNamedParameter( |
| parameter, |
| parameterElement, |
| constructorElement, |
| superInvocation, |
| )) { |
| var identifier = parameter.name?.lexeme; |
| if (identifier != null) { |
| identifiers.add(identifier); |
| } |
| } |
| } |
| } |
| |
| if (identifiers.isEmpty) return; |
| if (identifiers.length > 1) { |
| var msg = identifiers.quotedAndCommaSeparatedWithAnd; |
| rule.reportAtOffset( |
| errorRange.offset, |
| errorRange.length, |
| diagnosticCode: diag.useSuperParametersMultiple, |
| arguments: [msg], |
| ); |
| } else { |
| rule.reportAtOffset( |
| errorRange.offset, |
| errorRange.length, |
| diagnosticCode: diag.useSuperParametersSingle, |
| arguments: [identifiers.first], |
| ); |
| } |
| } |
| |
| @override |
| visitConstructorDeclaration(ConstructorDeclaration node) { |
| for (var initializer in node.initializers.reversed) { |
| if (initializer is SuperConstructorInvocation) { |
| check(node.errorRange, initializer, node.parameters, node.body); |
| return; |
| } |
| } |
| } |
| |
| @override |
| visitPrimaryConstructorDeclaration(PrimaryConstructorDeclaration node) { |
| if (node.body case var body?) { |
| for (var initializer in body.initializers.reversed) { |
| if (initializer is SuperConstructorInvocation) { |
| check(node.errorRange, initializer, node.formalParameters, body.body); |
| return; |
| } |
| } |
| } |
| } |
| |
| /// Checks if all positional arguments to the super-constructor invocation can |
| /// be converted to use super-parameters. |
| /// |
| /// Returns a list of convertible positional parameters or `null` if the |
| /// constructor is in a bad state (element types are not as expected, or |
| /// perhaps resolution is incomplete). |
| List<String>? _checkForConvertiblePositionalParams( |
| ConstructorElement constructorElement, |
| SuperConstructorInvocation superInvocation, |
| List<FormalParameter> parameters, |
| Set<FormalParameterElement> referencedParameters, |
| ) { |
| var positionalSuperArgs = <SimpleIdentifier>[]; |
| for (var arg in superInvocation.argumentList.arguments) { |
| if (arg is SimpleIdentifier) { |
| positionalSuperArgs.add(arg); |
| } else if (arg is! NamedArgument) { |
| return []; |
| } |
| } |
| |
| if (positionalSuperArgs.isEmpty) return []; |
| |
| var convertibleParams = <String>[]; |
| var matchedParamIndex = 0; |
| |
| var seenParamsInSuperConstructor = <Element>{}; |
| |
| // For each super arg, ensure there is a constructor param (in the right |
| // order). |
| for (var i = 0; i < positionalSuperArgs.length; ++i) { |
| var superArg = positionalSuperArgs[i]; |
| var paramPassedToSuper = superArg.element; |
| if (paramPassedToSuper is! FormalParameterElement) return null; |
| if (paramPassedToSuper.isNamed) return null; |
| |
| // Check for the case where a parameter passed to the super-constructor is |
| // used more than once. |
| if (!seenParamsInSuperConstructor.add(paramPassedToSuper)) { |
| return null; |
| } |
| |
| bool match = false; |
| for (var j = 0; j < parameters.length && !match; ++j) { |
| var parameter = parameters[j]; |
| if (parameter is FieldFormalParameter) return []; |
| if (parameter is SuperFormalParameter) return null; |
| var parameterElement = parameter.declaredFragment?.element; |
| if (parameterElement == null) return null; |
| if (referencedParameters.contains(parameterElement)) return []; |
| if (parameterElement == paramPassedToSuper) { |
| match = true; |
| var identifier = parameter.name?.lexeme; |
| if (identifier == null) return null; |
| convertibleParams.add(identifier); |
| // Ensure we're not out of order. |
| if (j < matchedParamIndex) return []; |
| matchedParamIndex = j; |
| } |
| } |
| } |
| |
| return convertibleParams; |
| } |
| |
| /// Return `true` if the named [parameter] can be converted into a super |
| /// initializing formal parameter. |
| bool _checkNamedParameter( |
| FormalParameter parameter, |
| FormalParameterElement parameterElement, |
| ConstructorElement superConstructor, |
| SuperConstructorInvocation superInvocation, |
| ) { |
| var superParameter = _correspondingNamedParameter( |
| superConstructor, |
| parameterElement, |
| ); |
| if (superParameter == null) return false; |
| |
| bool matchingArgument = false; |
| var arguments = superInvocation.argumentList.arguments; |
| for (var argument in arguments) { |
| if (argument is NamedArgument && |
| argument.name.lexeme == parameterElement.name) { |
| var expression = argument.argumentExpression; |
| if (expression is SimpleIdentifier && |
| expression.element == parameterElement) { |
| matchingArgument = true; |
| break; |
| } |
| } |
| } |
| if (!matchingArgument) { |
| // If the parameter isn't being passed to the super constructor, then |
| // don't lint. |
| return false; |
| } |
| |
| // Compare the types. |
| var superType = superParameter.type; |
| var thisType = parameterElement.type; |
| if (!context.typeSystem.isAssignableTo(superType, thisType)) { |
| // If the type of the parameter can't be assigned to the super parameter, |
| // then don't lint. |
| return false; |
| } |
| |
| return true; |
| } |
| |
| FormalParameterElement? _correspondingNamedParameter( |
| ConstructorElement superConstructor, |
| FormalParameterElement thisParameter, |
| ) { |
| for (var superParameter in superConstructor.formalParameters) { |
| if (superParameter.isNamed && superParameter.name == thisParameter.name) { |
| return superParameter; |
| } |
| } |
| return null; |
| } |
| } |