Skip to content

Commit 7d1d6d4

Browse files
authored
add unused_top_members_in_executable_libraries (dart-archive/linter#3513)
* add unused_top_members_in_executable_libraries * address review comments * address review comments * rename to unreachable_from_main * report on name * address review comments * address review comments * fix post-rebase
1 parent 840ea93 commit 7d1d6d4

File tree

4 files changed

+333
-0
lines changed

4 files changed

+333
-0
lines changed

example/all.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ linter:
186186
- unnecessary_string_interpolations
187187
- unnecessary_this
188188
- unnecessary_to_list_in_spreads
189+
- unreachable_from_main
189190
- unrelated_type_equality_checks
190191
- unsafe_html
191192
- use_build_context_synchronously

lib/src/rules.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ import 'rules/unnecessary_string_escapes.dart';
191191
import 'rules/unnecessary_string_interpolations.dart';
192192
import 'rules/unnecessary_this.dart';
193193
import 'rules/unnecessary_to_list_in_spreads.dart';
194+
import 'rules/unreachable_from_main.dart';
194195
import 'rules/unrelated_type_equality_checks.dart';
195196
import 'rules/unsafe_html.dart';
196197
import 'rules/use_build_context_synchronously.dart';
@@ -408,6 +409,7 @@ void registerLintRules({bool inTestMode = false}) {
408409
..register(UnnecessaryStringInterpolations())
409410
..register(UnnecessaryThis())
410411
..register(UnnecessaryToListInSpreads())
412+
..register(UnreachableFromMain())
411413
..register(UnrelatedTypeEqualityChecks())
412414
..register(UnsafeHtml())
413415
..register(UseBuildContextSynchronously(inTestMode: inTestMode))
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:collection';
6+
7+
import 'package:analyzer/dart/ast/ast.dart';
8+
import 'package:analyzer/dart/ast/visitor.dart';
9+
import 'package:analyzer/dart/element/element.dart';
10+
import 'package:analyzer/dart/element/type.dart';
11+
import 'package:collection/collection.dart';
12+
13+
import '../analyzer.dart';
14+
import '../util/dart_type_utilities.dart';
15+
16+
const _desc = 'Unreachable top-level members in executable libraries.';
17+
18+
const _details = r'''
19+
20+
Top-level members in an executable library should be used directly inside this
21+
library. An executable library is a library that contains a `main` top-level
22+
function or that contains a top-level function annotated with
23+
`@pragma('vm:entry-point')`). Executable libraries are not usually imported
24+
and it's better to avoid defining unused members.
25+
26+
This rule assumes that an executable library isn't imported by other files
27+
except to execute its `main` function.
28+
29+
**BAD:**
30+
31+
```dart
32+
main() {}
33+
void f() {}
34+
```
35+
36+
**GOOD:**
37+
38+
```dart
39+
main() {
40+
f();
41+
}
42+
void f() {}
43+
```
44+
45+
''';
46+
47+
class UnreachableFromMain extends LintRule {
48+
UnreachableFromMain()
49+
: super(
50+
name: 'unreachable_from_main',
51+
description: _desc,
52+
details: _details,
53+
group: Group.style,
54+
maturity: Maturity.experimental,
55+
);
56+
57+
@override
58+
void registerNodeProcessors(
59+
NodeLintRegistry registry,
60+
LinterContext context,
61+
) {
62+
var visitor = _Visitor(this);
63+
registry.addCompilationUnit(this, visitor);
64+
}
65+
}
66+
67+
class _Visitor extends SimpleAstVisitor<void> {
68+
_Visitor(this.rule);
69+
70+
final LintRule rule;
71+
72+
@override
73+
void visitCompilationUnit(CompilationUnit node) {
74+
// TODO(a14n): add support of libs with parts
75+
if (node.directives.whereType<PartOfDirective>().isNotEmpty) return;
76+
if (node.directives.whereType<PartDirective>().isNotEmpty) return;
77+
78+
var topDeclarations = node.declarations
79+
.expand((e) => [
80+
if (e is TopLevelVariableDeclaration)
81+
...e.variables.variables
82+
else
83+
e,
84+
])
85+
.toSet();
86+
87+
var entryPoints = topDeclarations.where(_isEntryPoint).toList();
88+
if (entryPoints.isEmpty) return;
89+
90+
var declarationByElement = <Element, Declaration>{};
91+
for (var declaration in topDeclarations) {
92+
var element = declaration.declaredElement;
93+
if (element != null) {
94+
if (element is TopLevelVariableElement) {
95+
declarationByElement[element] = declaration;
96+
var getter = element.getter;
97+
if (getter != null) declarationByElement[getter] = declaration;
98+
var setter = element.setter;
99+
if (setter != null) declarationByElement[setter] = declaration;
100+
} else {
101+
declarationByElement[element] = declaration;
102+
}
103+
}
104+
}
105+
106+
// The following map contains for every declaration the set of the
107+
// declarations it references.
108+
var dependencies = Map<Declaration, Set<Declaration>>.fromIterable(
109+
topDeclarations,
110+
value: (declaration) =>
111+
DartTypeUtilities.traverseNodesInDFS(declaration as Declaration)
112+
.expand((e) => [
113+
if (e is SimpleIdentifier) e.staticElement,
114+
// with `id++` staticElement of `id` is null
115+
if (e is CompoundAssignmentExpression) ...[
116+
e.readElement,
117+
e.writeElement,
118+
],
119+
])
120+
.whereNotNull()
121+
.map((e) => e.thisOrAncestorMatching((a) =>
122+
a.enclosingElement3 == null ||
123+
a.enclosingElement3 is CompilationUnitElement))
124+
.map((e) => declarationByElement[e])
125+
.whereNotNull()
126+
.where((e) => e != declaration)
127+
.toSet(),
128+
);
129+
130+
var usedMembers = entryPoints.toSet();
131+
// The following variable will be used to visit every reachable declaration
132+
// starting from entry-points. At every loop an element is removed. This
133+
// element is marked as used and we add its dependencies in the declaration
134+
// list to traverse. Once this list is empty `usedMembers` contains every
135+
// declarations reachable from an entry-point.
136+
var toTraverse = Queue.of(usedMembers);
137+
while (toTraverse.isNotEmpty) {
138+
var declaration = toTraverse.removeLast();
139+
for (var dep in dependencies[declaration]!) {
140+
if (usedMembers.add(dep)) {
141+
toTraverse.add(dep);
142+
}
143+
}
144+
}
145+
146+
var unusedMembers = topDeclarations.difference(usedMembers).where((e) {
147+
var element = e.declaredElement;
148+
return element != null &&
149+
element.isPublic &&
150+
!element.hasVisibleForTesting;
151+
});
152+
153+
for (var member in unusedMembers) {
154+
if (member is NamedCompilationUnitMember) {
155+
rule.reportLintForToken(member.name2);
156+
} else if (member is VariableDeclaration) {
157+
rule.reportLintForToken(member.name2);
158+
} else if (member is ExtensionDeclaration) {
159+
rule.reportLintForToken(
160+
member.name2 ?? member.firstTokenAfterCommentAndMetadata);
161+
} else {
162+
rule.reportLintForToken(member.firstTokenAfterCommentAndMetadata);
163+
}
164+
}
165+
}
166+
167+
bool _isEntryPoint(Declaration e) =>
168+
e is FunctionDeclaration &&
169+
(e.name2.lexeme == 'main' || e.metadata.any(_isPragmaVmEntry));
170+
171+
bool _isPragmaVmEntry(Annotation annotation) {
172+
if (!annotation.isPragma) return false;
173+
var value = annotation.elementAnnotation?.computeConstantValue();
174+
if (value == null) return false;
175+
var name = value.getField('name');
176+
return name != null &&
177+
name.hasKnownValue &&
178+
name.toStringValue() == 'vm:entry-point';
179+
}
180+
}
181+
182+
extension on Element {
183+
bool get isPragma => (library?.isDartCore ?? false) && name == 'pragma';
184+
}
185+
186+
extension on Annotation {
187+
bool get isPragma {
188+
var element = elementAnnotation?.element;
189+
DartType type;
190+
if (element is ConstructorElement) {
191+
type = element.returnType;
192+
} else if (element is PropertyAccessorElement && element.isGetter) {
193+
type = element.returnType;
194+
} else {
195+
// Dunno what this is.
196+
return false;
197+
}
198+
return type is InterfaceType && type.element2.isPragma;
199+
}
200+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// test w/ `dart test -N unreachable_from_main`
6+
7+
import 'package:meta/meta.dart';
8+
9+
/// see [Comment]
10+
main() // OK
11+
{
12+
_f5();
13+
f1();
14+
f3(() {
15+
f4(b);
16+
});
17+
f4(b);
18+
usageInTypeBound();
19+
usageInFunctionType();
20+
usageInDefaultValue();
21+
usageInAnnotation();
22+
Future<C5>.value(C5()).extensionUsage();
23+
accessors();
24+
print(c2);
25+
}
26+
27+
class Comment {} // OK
28+
29+
const a = 1; // LINT
30+
const b = 1; // OK
31+
32+
final int //
33+
c1 = 1, // LINT
34+
c2 = 2; // OK
35+
36+
int v = 1; // LINT
37+
38+
typedef A = String; // LINT
39+
40+
class C {} // LINT
41+
42+
mixin M {} // LINT
43+
44+
enum E { e } // LINT
45+
46+
void f() {} // LINT
47+
48+
@visibleForTesting
49+
void forTest() {} // OK
50+
51+
void f1() // OK
52+
{
53+
f2();
54+
}
55+
56+
void f2() // OK
57+
{
58+
f1();
59+
}
60+
61+
void f3(Function f) {} // OK
62+
void f4(int p) {} // OK
63+
64+
int id = 0; // OK
65+
void _f5() {
66+
id++;
67+
}
68+
69+
@pragma('vm:entry-point')
70+
void f6() {} // OK
71+
72+
const entryPoint = pragma('vm:entry-point');
73+
@entryPoint
74+
void f7() {} // OK
75+
76+
@pragma('other')
77+
void f8() {} // LINT
78+
79+
// test accessors
80+
int get id9 => 0;
81+
void set id9(int value) {}
82+
void accessors() {
83+
id9 += 4; // usage
84+
}
85+
86+
// Usage in type bound
87+
class C1 {}
88+
89+
void usageInTypeBound<T extends C1>() {}
90+
91+
// Usage in Function type
92+
class C2 {}
93+
94+
void Function(C2)? usageInFunctionType() => null;
95+
96+
// Usage in default value
97+
class C3 {
98+
const C3();
99+
}
100+
101+
void usageInDefaultValue([Object? p = const C3()]) {}
102+
103+
// Usage in annotation
104+
class C4 {
105+
const C4();
106+
}
107+
108+
@C4()
109+
void usageInAnnotation() {}
110+
111+
// Usage in type parameter in extension `on` clause.
112+
class C5 {}
113+
114+
extension UsedPublicExt on Future<C5> {
115+
extensionUsage() {}
116+
}
117+
118+
// Usage in type parameter in extension `on` clause.
119+
class C6 {} //LINT
120+
121+
extension UnusedPublicExt on C6 //LINT
122+
{
123+
m() {}
124+
}
125+
126+
class C7 // LINT
127+
{
128+
C7();
129+
C7.named();
130+
}

0 commit comments

Comments
 (0)