blob: a799aea2b27f790b5c00f07d587f85a4f467d81c [file]
// 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();
}
}