| // Copyright (c) 2017, 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_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 '../analyzer.dart'; | |
| import '../diagnostic.dart' as diag; | |
| import '../extensions.dart'; | |
| const _desc = r'Use initializing formals when possible.'; | |
| class PreferInitializingFormals extends AnalysisRule { | |
| new() | |
| : super(name: LintNames.prefer_initializing_formals, description: _desc); | |
| @override | |
| DiagnosticCode get diagnosticCode => diag.preferInitializingFormals; | |
| @override | |
| void registerNodeProcessors( | |
| RuleVisitorRegistry registry, | |
| RuleContext context, | |
| ) { | |
| var visitor = _Visitor(this, context); | |
| registry.addConstructorDeclaration(this, visitor); | |
| registry.addPrimaryConstructorDeclaration(this, visitor); | |
| } | |
| } | |
| /// Reports lints for a single constructor declaration. | |
| class _ConstructorChecker { | |
| final AnalysisRule _rule; | |
| final ConstructorFragment? _constructorFragment; | |
| final FormalParameterList _parameterList; | |
| final NodeList<ConstructorInitializer>? _initializers; | |
| final FunctionBody? _body; | |
| /// The elements for each constructor formal parameter. | |
| final List<Element?> _parameters; | |
| /// Elements for constructor parameters that are already initializing formals. | |
| final Set<Element> _initializingParameters = {}; | |
| /// Group the AST nodes where we might show the lint by the field they | |
| /// initialize. If there are multiple places that initialize the same field, | |
| /// don't lint any of them. It would be confusing, because only one of them | |
| /// could be converted to an initializing formal and the linter doesn't know | |
| /// which one should be. | |
| final Map<Element, List<AstNode>> _nodesToLintByField = {}; | |
| /// True if the "private_named_parameters" feature is enabled in the | |
| /// surrounding library. | |
| final bool _privateNamedParametersEnabled; | |
| new( | |
| this._rule, | |
| this._constructorFragment, | |
| this._parameterList, | |
| this._initializers, | |
| this._body, { | |
| required this._privateNamedParametersEnabled, | |
| }) : _parameters = _parameterList.parameters | |
| .where((param) => param is! SuperFormalParameter) | |
| .map((param) => param.declaredFragment?.element) | |
| .toList(); | |
| void check() { | |
| // Don't lint initializers from parameters that are already initializing | |
| // formals. | |
| for (var parameterFragment in _parameterList.parameterFragments) { | |
| if (parameterFragment == null) continue; | |
| var parameter = parameterFragment.element; | |
| // TODO(rnystrom): Handle declaring parameters for primary constructors | |
| // here too. | |
| if (parameter is FieldFormalParameterElement) { | |
| _initializingParameters.add(parameter); | |
| } | |
| } | |
| // Look for constructor initializers to lint. | |
| if (_initializers != null) { | |
| for (var constructorInitializer in _initializers) { | |
| // Must be a public field initializer. | |
| if (constructorInitializer is! ConstructorFieldInitializer) continue; | |
| // Must be initializing from a variable. | |
| var initializerExpression = constructorInitializer.expression; | |
| if (initializerExpression is! SimpleIdentifier) continue; | |
| _checkInitializer( | |
| constructorInitializer, | |
| constructorInitializer.fieldName.element, | |
| initializerExpression.element, | |
| ); | |
| } | |
| } | |
| // If the constructor has a block body, look for field assignments in it. | |
| if (_body case BlockFunctionBody block) { | |
| for (var statement in block.block.statements) { | |
| // Must be an expression statement containing an assignment of the form | |
| // "this.x = x;" for some "x". | |
| // | |
| // We require this exact syntactic form to ensure that the field being | |
| // initialized is not just on this class but on the instance being | |
| // constructed by this constructor. | |
| if (statement case ExpressionStatement( | |
| expression: AssignmentExpression( | |
| leftHandSide: PropertyAccess(target: ThisExpression()), | |
| ) && | |
| var assignment, | |
| )) { | |
| _checkInitializer( | |
| assignment, | |
| assignment.writeElement?.canonicalElement2, | |
| assignment.rightHandSide.canonicalElement, | |
| ); | |
| } | |
| } | |
| } | |
| _nodesToLintByField.forEach((field, nodes) { | |
| for (var lintNode in nodes) { | |
| _rule.reportAtNode(lintNode, arguments: [field.name!]); | |
| } | |
| }); | |
| } | |
| /// If the initialization of [field] with [parameter] should be linted, adds | |
| /// it to [_nodesToLintByField]. | |
| void _checkInitializer(AstNode node, Element? field, Element? parameter) { | |
| // Must be assigning to an instance field. | |
| if (field is! FieldElement) return; | |
| if (field.isStatic) return; | |
| if (_initializingParameters.contains(parameter)) return; | |
| // Must be an actual field and not a setter. | |
| if (!field.isOriginDeclaration) return; | |
| // Must be assigning from a constructor parameter with a matching name. | |
| if (parameter is! FormalParameterElement) return; | |
| if (!_parameters.contains(parameter)) return; | |
| // An initializing formal is required to have a type that's a subtype of the | |
| // field type (assignability is not sufficient). If this requirement isn't | |
| // met, don't lint, because the corresponding fix will lead to a | |
| // compile-time error. | |
| var library = parameter.library!; | |
| if (!library.typeSystem.isSubtypeOf(parameter.type, field.type)) { | |
| return; | |
| } | |
| // Must be the same name (modulo privacy for private named parameters). | |
| if (field.isPrivate) { | |
| // Never lint on private names if the feature isn't supported. | |
| if (!_privateNamedParametersEnabled) return; | |
| // Only lint on private named parameters. | |
| if (parameter.isPositional) return; | |
| // Allow initializing a private field from a parameter with the same | |
| // private name or the corresponding public one. | |
| if (field.name != parameter.name && field.name != '_${parameter.name}') { | |
| return; | |
| } | |
| } else if (field.name != parameter.name) { | |
| return; // The name must match exactly. | |
| } | |
| // Must be initializing a field on the surrounding class and not an | |
| // inherited one. | |
| if (field.enclosingElement != | |
| _constructorFragment?.element.enclosingElement) { | |
| return; | |
| } | |
| // There can't be any other references to the parameter. If there are, it's | |
| // possible removing the initializer/assignment and moving it up to be an | |
| // initializing formal could be a semantic change. | |
| var visitor = _ReferenceCounter(parameter); | |
| // Visit the initializers and body directly so that we ignore references in | |
| // the doc comment. | |
| _initializers?.accept(visitor); | |
| _body?.accept(visitor); | |
| if (visitor.count > 1) return; | |
| _nodesToLintByField.putIfAbsent(field, () => []).add(node); | |
| } | |
| } | |
| /// Counts references in the visited AST to a given parameter. | |
| class _ReferenceCounter extends RecursiveAstVisitor<void> { | |
| final FormalParameterElement parameterElement; | |
| int count = 0; | |
| new(this.parameterElement); | |
| @override | |
| void visitSimpleIdentifier(SimpleIdentifier node) { | |
| if (node.element == parameterElement) { | |
| count++; | |
| } | |
| } | |
| } | |
| class _Visitor(final AnalysisRule _rule, final RuleContext _context) | |
| extends SimpleAstVisitor<void> { | |
| @override | |
| void visitConstructorDeclaration(ConstructorDeclaration node) { | |
| // Skip factory constructors. | |
| // https://github.com/dart-lang/linter/issues/2441 | |
| if (node.factoryKeyword != null) return; | |
| _ConstructorChecker( | |
| _rule, | |
| node.declaredFragment, | |
| node.parameters, | |
| node.initializers, | |
| node.body, | |
| privateNamedParametersEnabled: _context.isFeatureEnabled( | |
| Feature.private_named_parameters, | |
| ), | |
| ).check(); | |
| } | |
| @override | |
| void visitPrimaryConstructorDeclaration(PrimaryConstructorDeclaration node) { | |
| var body = node.body; | |
| _ConstructorChecker( | |
| _rule, | |
| node.declaredFragment, | |
| node.formalParameters, | |
| body?.initializers, | |
| body?.body, | |
| privateNamedParametersEnabled: _context.isFeatureEnabled( | |
| Feature.private_named_parameters, | |
| ), | |
| ).check(); | |
| } | |
| } |