Skip to content

Commit e0f73d7

Browse files
authored
Support prefer_final_fields in libs with parts (dart-archive/linter#4568)
1 parent 8bd5472 commit e0f73d7

File tree

2 files changed

+104
-56
lines changed

2 files changed

+104
-56
lines changed

lib/src/rules/prefer_final_fields.dart

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'dart:collection';
6-
75
import 'package:analyzer/dart/ast/ast.dart';
86
import 'package:analyzer/dart/ast/token.dart';
97
import 'package:analyzer/dart/ast/visitor.dart';
@@ -83,17 +81,6 @@ class NotAssignedInAllConstructors {
8381
```
8482
''';
8583

86-
bool _containedInFormal(Element element, FormalParameter formal) {
87-
var formalField = formal.declaredElement;
88-
return formalField is FieldFormalParameterElement &&
89-
formalField.field == element;
90-
}
91-
92-
bool _containedInInitializer(
93-
Element element, ConstructorInitializer initializer) =>
94-
initializer is ConstructorFieldInitializer &&
95-
initializer.fieldName.canonicalElement == element;
96-
9784
class PreferFinalFields extends LintRule {
9885
static const LintCode code = LintCode(
9986
'prefer_final_fields', "The private field {0} could be 'final'.",
@@ -112,16 +99,39 @@ class PreferFinalFields extends LintRule {
11299
@override
113100
void registerNodeProcessors(
114101
NodeLintRegistry registry, LinterContext context) {
115-
var visitor = _Visitor(this);
102+
var visitor = _Visitor(this, context);
116103
registry.addCompilationUnit(this, visitor);
117-
registry.addFieldDeclaration(this, visitor);
118104
}
119105
}
120106

121-
class _MutatedFieldsCollector extends RecursiveAstVisitor<void> {
122-
final Set<FieldElement> _mutatedFields;
107+
class _DeclarationsCollector extends RecursiveAstVisitor<void> {
108+
final fields = <FieldElement, VariableDeclaration>{};
109+
110+
@override
111+
void visitFieldDeclaration(FieldDeclaration node) {
112+
if (node.parent is EnumDeclaration) return;
113+
if (node.fields.isFinal || node.fields.isConst) {
114+
return;
115+
}
116+
117+
for (var variable in node.fields.variables) {
118+
var element = variable.declaredElement;
119+
if (element is FieldElement &&
120+
element.isPrivate &&
121+
!element.overridesField) {
122+
fields[element] = variable;
123+
}
124+
}
125+
}
126+
}
127+
128+
class _FieldMutationFinder extends RecursiveAstVisitor<void> {
129+
/// The collection of fields declared in this library.
130+
///
131+
/// This visitor removes a field when it finds that it is assigned anywhere.
132+
final Map<FieldElement, VariableDeclaration> _fields;
123133

124-
_MutatedFieldsCollector(this._mutatedFields);
134+
_FieldMutationFinder(this._fields);
125135

126136
@override
127137
void visitAssignmentExpression(AssignmentExpression node) {
@@ -148,67 +158,57 @@ class _MutatedFieldsCollector extends RecursiveAstVisitor<void> {
148158
void _addMutatedFieldElement(CompoundAssignmentExpression assignment) {
149159
var element = assignment.writeElement?.canonicalElement;
150160
if (element is FieldElement) {
151-
_mutatedFields.add(element);
161+
_fields.remove(element);
152162
}
153163
}
154164
}
155165

156166
class _Visitor extends SimpleAstVisitor<void> {
157167
final LintRule rule;
158168

159-
final Set<FieldElement> _mutatedFields = HashSet<FieldElement>();
169+
final LinterContext context;
160170

161-
_Visitor(this.rule);
171+
_Visitor(this.rule, this.context);
162172

163173
@override
164174
void visitCompilationUnit(CompilationUnit node) {
165-
node.accept(_MutatedFieldsCollector(_mutatedFields));
166-
}
167-
168-
@override
169-
void visitFieldDeclaration(FieldDeclaration node) {
170-
if (node.parent is EnumDeclaration) return;
175+
var declarationsCollector = _DeclarationsCollector();
176+
node.accept(declarationsCollector);
177+
var fields = declarationsCollector.fields;
171178

172-
var fields = node.fields;
173-
if (fields.isFinal || fields.isConst) {
174-
return;
179+
var fieldMutationFinder = _FieldMutationFinder(fields);
180+
for (var unit in context.allUnits) {
181+
unit.unit.accept(fieldMutationFinder);
175182
}
176183

177-
for (var variable in fields.variables) {
178-
var element = variable.declaredElement;
184+
for (var MapEntry(key: field, value: variable) in fields.entries) {
185+
// TODO(srawlins): We could look at the constructors once and store a set
186+
// of which fields are initialized by any, and a set of which fields are
187+
// initialized by all. This would concievably improve performance.
188+
var classDeclaration = variable.parent?.parent?.parent;
189+
var constructors = classDeclaration is ClassDeclaration
190+
? classDeclaration.members.whereType<ConstructorDeclaration>()
191+
: <ConstructorDeclaration>[];
179192

180-
if (element is PropertyInducingElement &&
181-
element.isPrivate &&
182-
!_mutatedFields.contains(element)) {
183-
bool fieldInConstructor(ConstructorDeclaration constructor) =>
184-
constructor.initializers.any((ConstructorInitializer initializer) =>
185-
_containedInInitializer(element, initializer)) ||
186-
constructor.parameters.parameters.any((FormalParameter formal) =>
187-
_containedInFormal(element, formal));
188-
189-
var classDeclaration = node.parent;
190-
var constructors = classDeclaration is ClassDeclaration
191-
? classDeclaration.members.whereType<ConstructorDeclaration>()
192-
: <ConstructorDeclaration>[];
193-
var isFieldInConstructors = constructors.any(fieldInConstructor);
194-
var isFieldInAllConstructors = constructors.every(fieldInConstructor);
195-
196-
if (element.overridesField()) continue;
197-
198-
if (isFieldInConstructors) {
199-
if (isFieldInAllConstructors) {
200-
rule.reportLint(variable, arguments: [variable.name.lexeme]);
201-
}
202-
} else if (element.hasInitializer) {
193+
var isSetInAnyConstructor = constructors
194+
.any((constructor) => field.isSetInConstructor(constructor));
195+
196+
if (isSetInAnyConstructor) {
197+
var isSetInEveryConstructor = constructors
198+
.every((constructor) => field.isSetInConstructor(constructor));
199+
200+
if (isSetInEveryConstructor) {
203201
rule.reportLint(variable, arguments: [variable.name.lexeme]);
204202
}
203+
} else if (field.hasInitializer) {
204+
rule.reportLint(variable, arguments: [variable.name.lexeme]);
205205
}
206206
}
207207
}
208208
}
209209

210210
extension on VariableElement {
211-
bool overridesField() {
211+
bool get overridesField {
212212
var enclosingElement = this.enclosingElement;
213213
if (enclosingElement is! InterfaceElement) return false;
214214

@@ -219,4 +219,20 @@ extension on VariableElement {
219219
.lookUpSetter2(name, inherited: true, library) !=
220220
null;
221221
}
222+
223+
bool isSetInConstructor(ConstructorDeclaration constructor) =>
224+
constructor.initializers.any(isSetInInitializer) ||
225+
constructor.parameters.parameters.any(isSetInParameter);
226+
227+
/// Whether `this` is initialized in [initializer].
228+
bool isSetInInitializer(ConstructorInitializer initializer) =>
229+
initializer is ConstructorFieldInitializer &&
230+
initializer.fieldName.canonicalElement == this;
231+
232+
/// Whether `this` is initialized with [parameter].
233+
bool isSetInParameter(FormalParameter parameter) {
234+
var formalField = parameter.declaredElement;
235+
return formalField is FieldFormalParameterElement &&
236+
formalField.field == this;
237+
}
222238
}

test/rules/prefer_final_fields_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,38 @@ class PreferFinalFieldsTest extends LintRuleTest {
1717
@override
1818
String get lintRule => 'prefer_final_fields';
1919

20+
test_assignedInPart() async {
21+
newFile('$testPackageLibPath/part.dart', r'''
22+
part of 'test.dart';
23+
int f(C c) {
24+
c._x = 1;
25+
return c._x;
26+
}
27+
''');
28+
await assertNoDiagnostics(r'''
29+
part 'part.dart';
30+
class C {
31+
int _x = 0;
32+
}
33+
''');
34+
}
35+
36+
test_declaredInPart() async {
37+
newFile('$testPackageLibPath/part.dart', r'''
38+
part of 'test.dart';
39+
class C {
40+
int _x = 0;
41+
}
42+
''');
43+
await assertNoDiagnostics(r'''
44+
part 'part.dart';
45+
int f(C c) {
46+
c._x = 1;
47+
return c._x;
48+
}
49+
''');
50+
}
51+
2052
test_enum() async {
2153
await assertDiagnostics(r'''
2254
enum A {

0 commit comments

Comments
 (0)