Skip to content
This repository was archived by the owner on Nov 20, 2024. It is now read-only.

add unused_top_members_in_executable_libraries #3513

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ linter:
- unnecessary_string_interpolations
- unnecessary_this
- unnecessary_to_list_in_spreads
- unreachable_from_main
- unrelated_type_equality_checks
- unsafe_html
- use_build_context_synchronously
Expand Down
2 changes: 2 additions & 0 deletions lib/src/rules.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ import 'rules/unnecessary_string_escapes.dart';
import 'rules/unnecessary_string_interpolations.dart';
import 'rules/unnecessary_this.dart';
import 'rules/unnecessary_to_list_in_spreads.dart';
import 'rules/unreachable_from_main.dart';
import 'rules/unrelated_type_equality_checks.dart';
import 'rules/unsafe_html.dart';
import 'rules/use_build_context_synchronously.dart';
Expand Down Expand Up @@ -408,6 +409,7 @@ void registerLintRules({bool inTestMode = false}) {
..register(UnnecessaryStringInterpolations())
..register(UnnecessaryThis())
..register(UnnecessaryToListInSpreads())
..register(UnreachableFromMain())
..register(UnrelatedTypeEqualityChecks())
..register(UnsafeHtml())
..register(UseBuildContextSynchronously(inTestMode: inTestMode))
Expand Down
200 changes: 200 additions & 0 deletions lib/src/rules/unreachable_from_main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright (c) 2022, 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 'dart:collection';

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';

import '../analyzer.dart';
import '../util/dart_type_utilities.dart';

const _desc = 'Unreachable top-level members in executable libraries.';

const _details = r'''

Top-level members in an executable library should be used directly inside this
library. An executable library is a library that contains a `main` top-level
function or that contains a top-level function annotated with
`@pragma('vm:entry-point')`). Executable libraries are not usually imported
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a stray ) here, at the end of the sentence.

and it's better to avoid defining unused members.

This rule assumes that an executable library isn't imported by other files
except to execute its `main` function.

**BAD:**

```dart
main() {}
void f() {}
```

**GOOD:**

```dart
main() {
f();
}
void f() {}
```

''';

class UnreachableFromMain extends LintRule {
UnreachableFromMain()
: super(
name: 'unreachable_from_main',
description: _desc,
details: _details,
group: Group.style,
maturity: Maturity.experimental,
);

@override
void registerNodeProcessors(
NodeLintRegistry registry,
LinterContext context,
) {
var visitor = _Visitor(this);
registry.addCompilationUnit(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
_Visitor(this.rule);

final LintRule rule;

@override
void visitCompilationUnit(CompilationUnit node) {
// TODO(a14n): add support of libs with parts
if (node.directives.whereType<PartOfDirective>().isNotEmpty) return;
if (node.directives.whereType<PartDirective>().isNotEmpty) return;

var topDeclarations = node.declarations
.expand((e) => [
if (e is TopLevelVariableDeclaration)
...e.variables.variables
else
e,
])
.toSet();

var entryPoints = topDeclarations.where(_isEntryPoint).toList();
if (entryPoints.isEmpty) return;

var declarationByElement = <Element, Declaration>{};
for (var declaration in topDeclarations) {
var element = declaration.declaredElement;
if (element != null) {
if (element is TopLevelVariableElement) {
declarationByElement[element] = declaration;
var getter = element.getter;
if (getter != null) declarationByElement[getter] = declaration;
var setter = element.setter;
if (setter != null) declarationByElement[setter] = declaration;
} else {
declarationByElement[element] = declaration;
}
}
}

// The following map contains for every declaration the set of the
// declarations it references.
var dependencies = Map<Declaration, Set<Declaration>>.fromIterable(
topDeclarations,
value: (declaration) =>
DartTypeUtilities.traverseNodesInDFS(declaration as Declaration)
.expand((e) => [
if (e is SimpleIdentifier) e.staticElement,
// with `id++` staticElement of `id` is null
if (e is CompoundAssignmentExpression) ...[
e.readElement,
e.writeElement,
],
])
.whereNotNull()
.map((e) => e.thisOrAncestorMatching((a) =>
a.enclosingElement3 == null ||
a.enclosingElement3 is CompilationUnitElement))
.map((e) => declarationByElement[e])
.whereNotNull()
.where((e) => e != declaration)
.toSet(),
);

var usedMembers = entryPoints.toSet();
// The following variable will be used to visit every reachable declaration
// starting from entry-points. At every loop an element is removed. This
// element is marked as used and we add its dependencies in the declaration
// list to traverse. Once this list is empty `usedMembers` contains every
// declarations reachable from an entry-point.
var toTraverse = Queue.of(usedMembers);
while (toTraverse.isNotEmpty) {
var declaration = toTraverse.removeLast();
for (var dep in dependencies[declaration]!) {
if (usedMembers.add(dep)) {
toTraverse.add(dep);
}
}
}

var unusedMembers = topDeclarations.difference(usedMembers).where((e) {
var element = e.declaredElement;
return element != null &&
element.isPublic &&
!element.hasVisibleForTesting;
});

for (var member in unusedMembers) {
if (member is NamedCompilationUnitMember) {
rule.reportLintForToken(member.name2);
} else if (member is VariableDeclaration) {
rule.reportLintForToken(member.name2);
} else if (member is ExtensionDeclaration) {
rule.reportLintForToken(
member.name2 ?? member.firstTokenAfterCommentAndMetadata);
} else {
rule.reportLintForToken(member.firstTokenAfterCommentAndMetadata);
}
}
}

bool _isEntryPoint(Declaration e) =>
e is FunctionDeclaration &&
(e.name2.lexeme == 'main' || e.metadata.any(_isPragmaVmEntry));

bool _isPragmaVmEntry(Annotation annotation) {
if (!annotation.isPragma) return false;
var value = annotation.elementAnnotation?.computeConstantValue();
if (value == null) return false;
var name = value.getField('name');
return name != null &&
name.hasKnownValue &&
name.toStringValue() == 'vm:entry-point';
}
}

extension on Element {
bool get isPragma => (library?.isDartCore ?? false) && name == 'pragma';
}

extension on Annotation {
bool get isPragma {
var element = elementAnnotation?.element;
DartType type;
if (element is ConstructorElement) {
type = element.returnType;
} else if (element is PropertyAccessorElement && element.isGetter) {
type = element.returnType;
} else {
// Dunno what this is.
return false;
}
return type is InterfaceType && type.element2.isPragma;
}
}
130 changes: 130 additions & 0 deletions test_data/rules/unreachable_from_main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) 2022, 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.

// test w/ `dart test -N unreachable_from_main`

import 'package:meta/meta.dart';

/// see [Comment]
main() // OK
{
_f5();
f1();
f3(() {
f4(b);
});
f4(b);
usageInTypeBound();
usageInFunctionType();
usageInDefaultValue();
usageInAnnotation();
Future<C5>.value(C5()).extensionUsage();
accessors();
print(c2);
}

class Comment {} // OK

const a = 1; // LINT
const b = 1; // OK

final int //
c1 = 1, // LINT
c2 = 2; // OK

int v = 1; // LINT

typedef A = String; // LINT

class C {} // LINT

mixin M {} // LINT

enum E { e } // LINT

void f() {} // LINT

@visibleForTesting
void forTest() {} // OK

void f1() // OK
{
f2();
}

void f2() // OK
{
f1();
}

void f3(Function f) {} // OK
void f4(int p) {} // OK

int id = 0; // OK
void _f5() {
id++;
}

@pragma('vm:entry-point')
void f6() {} // OK

const entryPoint = pragma('vm:entry-point');
@entryPoint
void f7() {} // OK

@pragma('other')
void f8() {} // LINT

// test accessors
int get id9 => 0;
void set id9(int value) {}
void accessors() {
id9 += 4; // usage
}

// Usage in type bound
class C1 {}

void usageInTypeBound<T extends C1>() {}

// Usage in Function type
class C2 {}

void Function(C2)? usageInFunctionType() => null;

// Usage in default value
class C3 {
const C3();
}

void usageInDefaultValue([Object? p = const C3()]) {}

// Usage in annotation
class C4 {
const C4();
}

@C4()
void usageInAnnotation() {}

// Usage in type parameter in extension `on` clause.
class C5 {}

extension UsedPublicExt on Future<C5> {
extensionUsage() {}
}

// Usage in type parameter in extension `on` clause.
class C6 {} //LINT

extension UnusedPublicExt on C6 //LINT
{
m() {}
}

class C7 // LINT
{
C7();
C7.named();
}