blob: 2ca508288596b8a24afa14a67d4be2e2a5a4f215 [file] [log] [blame]
// Copyright (c) 2016, 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/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import '../analyzer.dart';
import '../extensions.dart';
const _desc = r'Private field could be `final`.';
const _details = r'''
**DO** prefer declaring private fields as `final` if they are not reassigned
later in the library.
Declaring fields as `final` when possible is a good practice because it helps
avoid accidental reassignments and allows the compiler to do optimizations.
**BAD:**
```dart
class BadImmutable {
var _label = 'hola mundo! BadImmutable'; // LINT
var label = 'hola mundo! BadImmutable'; // OK
}
```
**BAD:**
```dart
class MultipleMutable {
var _label = 'hola mundo! GoodMutable', _offender = 'mumble mumble!'; // LINT
var _someOther; // LINT
MultipleMutable() : _someOther = 5;
MultipleMutable(this._someOther);
void changeLabel() {
_label= 'hello world! GoodMutable';
}
}
```
**GOOD:**
```dart
class GoodImmutable {
final label = 'hola mundo! BadImmutable', bla = 5; // OK
final _label = 'hola mundo! BadImmutable', _bla = 5; // OK
}
```
**GOOD:**
```dart
class GoodMutable {
var _label = 'hola mundo! GoodMutable';
void changeLabel() {
_label = 'hello world! GoodMutable';
}
}
```
**BAD:**
```dart
class AssignedInAllConstructors {
var _label; // LINT
AssignedInAllConstructors(this._label);
AssignedInAllConstructors.withDefault() : _label = 'Hello';
}
```
**GOOD:**
```dart
class NotAssignedInAllConstructors {
var _label; // OK
NotAssignedInAllConstructors();
NotAssignedInAllConstructors.withDefault() : _label = 'Hello';
}
```
''';
class PreferFinalFields extends LintRule {
static const LintCode code = LintCode(
'prefer_final_fields', "The private field {0} could be 'final'.",
correctionMessage: "Try making the field 'final'.");
PreferFinalFields()
: super(
name: 'prefer_final_fields',
description: _desc,
details: _details,
group: Group.style);
@override
LintCode get lintCode => code;
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this, context);
registry.addCompilationUnit(this, visitor);
}
}
class _DeclarationsCollector extends RecursiveAstVisitor<void> {
final fields = <FieldElement, VariableDeclaration>{};
@override
void visitFieldDeclaration(FieldDeclaration node) {
if (node.isInvalidExtensionTypeField) return;
if (node.parent is EnumDeclaration) return;
if (node.fields.isFinal || node.fields.isConst) {
return;
}
for (var variable in node.fields.variables) {
var element = variable.declaredElement;
if (element is FieldElement &&
element.isPrivate &&
!element.overridesField) {
fields[element] = variable;
}
}
}
}
class _FieldMutationFinder extends RecursiveAstVisitor<void> {
/// The collection of fields declared in this library.
///
/// This visitor removes a field when it finds that it is assigned anywhere.
final Map<FieldElement, VariableDeclaration> _fields;
_FieldMutationFinder(this._fields);
@override
void visitAssignmentExpression(AssignmentExpression node) {
_addMutatedFieldElement(node);
super.visitAssignmentExpression(node);
}
@override
void visitPostfixExpression(PostfixExpression node) {
_addMutatedFieldElement(node);
super.visitPostfixExpression(node);
}
@override
void visitPrefixExpression(PrefixExpression node) {
var operator = node.operator;
if (operator.type == TokenType.MINUS_MINUS ||
operator.type == TokenType.PLUS_PLUS) {
_addMutatedFieldElement(node);
}
super.visitPrefixExpression(node);
}
void _addMutatedFieldElement(CompoundAssignmentExpression assignment) {
var element = assignment.writeElement?.canonicalElement;
if (element is FieldElement) {
_fields.remove(element);
}
}
}
class _Visitor extends SimpleAstVisitor<void> {
final LintRule rule;
final LinterContext context;
_Visitor(this.rule, this.context);
@override
void visitCompilationUnit(CompilationUnit node) {
var declarationsCollector = _DeclarationsCollector();
node.accept(declarationsCollector);
var fields = declarationsCollector.fields;
var fieldMutationFinder = _FieldMutationFinder(fields);
for (var unit in context.allUnits) {
unit.unit.accept(fieldMutationFinder);
}
for (var MapEntry(key: field, value: variable) in fields.entries) {
// TODO(srawlins): We could look at the constructors once and store a set
// of which fields are initialized by any, and a set of which fields are
// initialized by all. This would conceivably improve performance.
var classDeclaration = variable.parent?.parent?.parent;
var constructors = classDeclaration is ClassDeclaration
? classDeclaration.members.whereType<ConstructorDeclaration>()
: <ConstructorDeclaration>[];
var isSetInAnyConstructor = constructors
.any((constructor) => field.isSetInConstructor(constructor));
if (isSetInAnyConstructor) {
var isSetInEveryConstructor = constructors
.every((constructor) => field.isSetInConstructor(constructor));
if (isSetInEveryConstructor) {
rule.reportLint(variable, arguments: [variable.name.lexeme]);
}
} else if (field.hasInitializer) {
rule.reportLint(variable, arguments: [variable.name.lexeme]);
}
}
}
}
extension on VariableElement {
bool get overridesField {
var enclosingElement = this.enclosingElement;
if (enclosingElement is! InterfaceElement) return false;
var library = this.library;
if (library == null) return false;
return enclosingElement.thisType
.lookUpSetter2(name, inherited: true, library) !=
null;
}
bool isSetInConstructor(ConstructorDeclaration constructor) =>
constructor.initializers.any(isSetInInitializer) ||
constructor.parameters.parameters.any(isSetInParameter);
/// Whether `this` is initialized in [initializer].
bool isSetInInitializer(ConstructorInitializer initializer) =>
initializer is ConstructorFieldInitializer &&
initializer.fieldName.canonicalElement == this;
/// Whether `this` is initialized with [parameter].
bool isSetInParameter(FormalParameter parameter) {
var formalField = parameter.declaredElement;
return formalField is FieldFormalParameterElement &&
formalField.field == this;
}
}