blob: 7ef4713389dd32bb53e2cc866a35113360a7790b [file] [log] [blame]
// 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 '../analyzer.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
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 set of the declarations which each top-level declaration references.
var dependencies = <Declaration, Set<Declaration>>{};
for (var declaration in topDeclarations) {
var visitor = _IdentifierVisitor(declarationByElement);
declaration.accept(visitor);
dependencies[declaration] = visitor.declarations;
}
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 declarationsToCheck = Queue.of(usedMembers);
while (declarationsToCheck.isNotEmpty) {
var declaration = declarationsToCheck.removeLast();
for (var dep in dependencies[declaration]!) {
if (usedMembers.add(dep)) {
declarationsToCheck.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.name);
} else if (member is VariableDeclaration) {
rule.reportLintForToken(member.name);
} else if (member is ExtensionDeclaration) {
rule.reportLintForToken(
member.name ?? member.firstTokenAfterCommentAndMetadata);
} else {
rule.reportLintForToken(member.firstTokenAfterCommentAndMetadata);
}
}
}
bool _isEntryPoint(Declaration e) =>
e is FunctionDeclaration &&
(e.name.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';
}
}
/// A visitor which gathers the declarations of the identifiers it visits.
class _IdentifierVisitor extends RecursiveAstVisitor {
Map<Element, Declaration> declarationMap;
Set<Declaration> declarations = {};
_IdentifierVisitor(this.declarationMap);
@override
void visitAssignmentExpression(AssignmentExpression node) {
_visitCompoundAssignmentExpression(node);
super.visitAssignmentExpression(node);
}
@override
void visitPostfixExpression(PostfixExpression node) {
_visitCompoundAssignmentExpression(node);
super.visitPostfixExpression(node);
}
@override
void visitPrefixExpression(PrefixExpression node) {
_visitCompoundAssignmentExpression(node);
super.visitPrefixExpression(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var e = node.staticElement;
if (e != null) {
_addDeclaration(e);
}
super.visitSimpleIdentifier(node);
}
void _visitCompoundAssignmentExpression(CompoundAssignmentExpression node) {
var readElement = node.readElement;
if (readElement != null) {
_addDeclaration(readElement);
}
var writeElement = node.writeElement;
if (writeElement != null) {
_addDeclaration(writeElement);
}
}
/// Adds the declaration of the top-level element which contains [element] to
/// [declarations], if it is found in [declarationMap].
void _addDeclaration(Element element) {
var enclosingElement = element.thisOrAncestorMatching((a) =>
a.enclosingElement == null ||
a.enclosingElement is CompilationUnitElement);
var enclosingDeclaration = declarationMap[enclosingElement];
if (enclosingDeclaration != null) {
declarations.add(enclosingDeclaration);
}
}
}
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.element.isPragma;
}
}