| // 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 'package:pub_semver/pub_semver.dart'; |
| |
| import '../analyzer.dart'; |
| import '../linter_lint_codes.dart'; |
| |
| const _desc = 'Unreachable top-level members in executable libraries.'; |
| |
| const _details = r''' |
| Any member declared in an executable library should be used directly inside that |
| 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 libraries |
| 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, |
| categories: {LintRuleCategory.unusedCode}, |
| state: State.stable(since: Version(3, 1, 0)), |
| ); |
| |
| @override |
| LintCode get lintCode => LinterLintCode.unreachable_from_main; |
| |
| @override |
| void registerNodeProcessors( |
| NodeLintRegistry registry, |
| LinterContext context, |
| ) { |
| var visitor = _Visitor(this, context); |
| registry.addCompilationUnit(this, visitor); |
| } |
| } |
| |
| /// This gathers all declarations which we may wish to report on. |
| class _DeclarationGatherer { |
| final LinterContext linterContext; |
| |
| /// All declarations which we may wish to report on. |
| final Set<Declaration> declarations = {}; |
| |
| _DeclarationGatherer({ |
| required this.linterContext, |
| }); |
| |
| void addDeclarations(CompilationUnit node) { |
| for (var declaration in node.declarations) { |
| if (declaration is TopLevelVariableDeclaration) { |
| declarations.addAll(declaration.variables.variables); |
| } else { |
| declarations.add(declaration); |
| var declaredElement = declaration.declaredElement; |
| if (declaredElement == null || declaredElement.isPrivate) { |
| continue; |
| } |
| if (declaration is ClassDeclaration) { |
| _addMembers( |
| containerElement: declaration.declaredElement, |
| members: declaration.members, |
| ); |
| } else if (declaration is EnumDeclaration) { |
| _addMembers( |
| containerElement: declaration.declaredElement, |
| members: declaration.members, |
| ); |
| } else if (declaration is ExtensionDeclaration) { |
| _addMembers( |
| containerElement: null, |
| members: declaration.members, |
| ); |
| } else if (declaration is ExtensionTypeDeclaration) { |
| _addMembers( |
| containerElement: null, |
| members: declaration.members, |
| ); |
| } else if (declaration is MixinDeclaration) { |
| _addMembers( |
| containerElement: declaration.declaredElement, |
| members: declaration.members, |
| ); |
| } |
| } |
| } |
| } |
| |
| void _addMembers({ |
| required Element? containerElement, |
| required List<ClassMember> members, |
| }) { |
| bool isOverride(String rawName) { |
| if (containerElement is! InterfaceElement) { |
| return false; |
| } |
| var libraryUri = containerElement.library.source.uri; |
| var name = Name(libraryUri, rawName); |
| var inheritance = linterContext.inheritanceManager; |
| return inheritance.getOverridden2(containerElement, name) != null; |
| } |
| |
| for (var member in members) { |
| switch (member) { |
| case ConstructorDeclaration(): |
| var e = member.declaredElement; |
| if (e != null && e.isPublic && member.parent is! EnumDeclaration) { |
| declarations.add(member); |
| } |
| case FieldDeclaration(): |
| for (var field in member.fields.variables) { |
| var element = field.declaredElement; |
| if (element != null && element.isPublic) { |
| if (!isOverride(element.name)) { |
| declarations.add(field); |
| } |
| } |
| } |
| case MethodDeclaration(): |
| var element = member.declaredElement; |
| if (element != null && element.isPublic) { |
| var rawName = member.name.lexeme; |
| var isTestMethod = rawName.startsWith('test_') || |
| rawName.startsWith('solo_test_') || |
| rawName == 'setUp' || |
| rawName == 'tearDown'; |
| if (!isOverride(element.name) && !isTestMethod) { |
| declarations.add(member); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /// A visitor which gathers the declarations of the "references" it visits. |
| /// |
| /// "References" are most often [SimpleIdentifier]s, but can also be other |
| /// nodes which refer to a declaration. |
| class _ReferenceVisitor extends RecursiveAstVisitor { |
| Map<Element, Declaration> declarationMap; |
| |
| Set<Declaration> declarations = {}; |
| |
| /// References from patterns should not be counted. |
| int _patternLevel = 0; |
| |
| _ReferenceVisitor(this.declarationMap); |
| |
| @override |
| void visitAnnotation(Annotation node) { |
| var e = node.element; |
| if (e != null) { |
| _addDeclaration(e); |
| } |
| super.visitAnnotation(node); |
| } |
| |
| @override |
| void visitAssignmentExpression(AssignmentExpression node) { |
| _visitCompoundAssignmentExpression(node); |
| super.visitAssignmentExpression(node); |
| } |
| |
| @override |
| void visitClassDeclaration(ClassDeclaration node) { |
| _addNamedType(node.extendsClause?.superclass); |
| _addNamedTypes(node.withClause?.mixinTypes); |
| _addNamedTypes(node.implementsClause?.interfaces); |
| |
| var element = node.declaredElement; |
| if (element != null) { |
| var hasConstructors = |
| node.members.any((e) => e is ConstructorDeclaration); |
| if (!hasConstructors) { |
| // The default constructor will have an implicit super-initializer to |
| // the super-type's unnamed constructor. |
| _addDefaultSuperConstructorDeclaration(node); |
| } |
| |
| var metadata = element.metadata; |
| // This for-loop style is copied from analyzer's `hasX` getters on |
| // [Element]. |
| for (var i = 0; i < metadata.length; i++) { |
| var annotation = metadata[i].element; |
| if (annotation is PropertyAccessorElement && |
| annotation.name == 'reflectiveTest' && |
| annotation.library.name == 'test_reflective_loader') { |
| // The class is instantiated through the use of mirrors in |
| // 'test_reflective_loader'. |
| var unnamedConstructor = element.constructors |
| .firstWhereOrNull((constructor) => constructor.name == ''); |
| if (unnamedConstructor != null) { |
| _addDeclaration(unnamedConstructor); |
| } |
| } |
| } |
| } |
| super.visitClassDeclaration(node); |
| } |
| |
| @override |
| visitConstantPattern(ConstantPattern node) { |
| _patternLevel++; |
| try { |
| return super.visitConstantPattern(node); |
| } finally { |
| _patternLevel--; |
| } |
| } |
| |
| @override |
| void visitConstructorDeclaration(ConstructorDeclaration node) { |
| // If a constructor in a class declaration does not have an explicit |
| // super-initializer (or redirection?) then it has an implicit |
| // super-initializer to the super-type's unnamed constructor. |
| var hasSuperInitializer = |
| node.initializers.any((e) => e is SuperConstructorInvocation); |
| if (!hasSuperInitializer) { |
| var enclosingClass = node.parent; |
| if (enclosingClass is ClassDeclaration) { |
| _addDefaultSuperConstructorDeclaration(enclosingClass); |
| } |
| } |
| super.visitConstructorDeclaration(node); |
| } |
| |
| @override |
| void visitConstructorName(ConstructorName node) { |
| var e = node.staticElement; |
| if (e != null && _patternLevel == 0) { |
| _addDeclaration(e); |
| var type = node.type.element; |
| if (type != null) { |
| _addDeclaration(type); |
| } |
| } |
| super.visitConstructorName(node); |
| } |
| |
| @override |
| void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { |
| _addNamedTypes(node.implementsClause?.interfaces); |
| |
| super.visitExtensionTypeDeclaration(node); |
| } |
| |
| @override |
| void visitMethodDeclaration(MethodDeclaration node) { |
| if (node.name.lexeme == 'toJson' && !node.isStatic) { |
| // The 'dart:convert' library uses dynamic invocation to call `toJson` on |
| // arbitrary objects. Any declaration of `toJson` is automatically |
| // reachable. |
| var element = node.declaredElement; |
| if (element != null) { |
| _addDeclaration(element); |
| } |
| } |
| super.visitMethodDeclaration(node); |
| } |
| |
| @override |
| void visitNamedType(NamedType node) { |
| var element = node.element; |
| if (element == null) { |
| return; |
| } |
| |
| var nodeIsInTypeArgument = |
| node.thisOrAncestorOfType<TypeArgumentList>() != null; |
| |
| if ( |
| // Any reference to a typedef marks it as reachable, since structural |
| // typing is used to match against objects. |
| node.type?.alias != null || |
| // Any reference to an extension type marks it as reachable, since |
| // casting can be used to instantiate the type. |
| node.type?.element is ExtensionTypeElement || |
| nodeIsInTypeArgument || |
| // A reference to any type in an external variable declaration marks |
| // that type as reachable, since the external implementation can |
| // instantiate it. |
| node.isInExternalVariableTypeOrFunctionReturnType) { |
| _addDeclaration(element); |
| } |
| |
| // Intentionally do not add the declaration of non-alias named types, as a |
| // reference to such a type in a [TypeAnnotation] is not good enough to |
| // count as "reachable". Marking a type as reachable only because it was |
| // seen in a type annotation would be a miscategorization if the type is |
| // never instantiated or subtyped. |
| |
| var typeArguments = node.typeArguments; |
| if (typeArguments != null) { |
| for (var typeArgument in typeArguments.arguments) { |
| typeArgument.accept(this); |
| } |
| } |
| } |
| |
| @override |
| void visitPatternField(PatternField node) { |
| var e = node.element; |
| if (e != null) { |
| _addDeclaration(e); |
| } |
| super.visitPatternField(node); |
| } |
| |
| @override |
| void visitPostfixExpression(PostfixExpression node) { |
| _visitCompoundAssignmentExpression(node); |
| super.visitPostfixExpression(node); |
| } |
| |
| @override |
| void visitPrefixExpression(PrefixExpression node) { |
| _visitCompoundAssignmentExpression(node); |
| super.visitPrefixExpression(node); |
| } |
| |
| @override |
| void visitRedirectingConstructorInvocation( |
| RedirectingConstructorInvocation node) { |
| var element = node.staticElement; |
| if (element != null) { |
| _addDeclaration(element); |
| } |
| super.visitRedirectingConstructorInvocation(node); |
| } |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| if (!node.inDeclarationContext()) { |
| var e = node.staticElement; |
| if (e != null) { |
| _addDeclaration(e); |
| } |
| } |
| super.visitSimpleIdentifier(node); |
| } |
| |
| @override |
| void visitSuperConstructorInvocation(SuperConstructorInvocation node) { |
| var e = node.staticElement; |
| if (e != null) { |
| _addDeclaration(e); |
| } |
| super.visitSuperConstructorInvocation(node); |
| } |
| |
| @override |
| void visitVariableDeclaration(VariableDeclaration node) { |
| var parent = node.parent; |
| if (parent is VariableDeclarationList) { |
| var type = parent.type; |
| if (type != null) { |
| type.accept(this); |
| } |
| } |
| super.visitVariableDeclaration(node); |
| } |
| |
| /// Adds the declaration of the top-level element which contains [element] to |
| /// [declarations], if it is found in [declarationMap]. |
| /// |
| /// Also adds the declaration of [element] if it is a public static accessor |
| /// or static method on a public top-level element. |
| void _addDeclaration(Element element) { |
| // First add the enclosing top-level declaration. |
| var enclosingTopLevelElement = element.thisOrAncestorMatching((a) => |
| a.enclosingElement3 == null || |
| a.enclosingElement3 is CompilationUnitElement); |
| var enclosingTopLevelDeclaration = declarationMap[enclosingTopLevelElement]; |
| if (enclosingTopLevelDeclaration != null) { |
| declarations.add(enclosingTopLevelDeclaration); |
| } |
| |
| // Also add [element]'s declaration if it is a constructor, static accessor, |
| // or static method. |
| if (element.isPrivate) { |
| return; |
| } |
| var enclosingElement = element.enclosingElement3; |
| if (enclosingElement == null || enclosingElement.isPrivate) { |
| return; |
| } |
| if (enclosingElement is InterfaceElement || |
| enclosingElement is ExtensionElement || |
| enclosingElement is ExtensionTypeElement) { |
| var declarationElement = element.declaration; |
| var declaration = declarationMap[declarationElement]; |
| if (declaration != null) { |
| declarations.add(declaration); |
| } |
| } |
| } |
| |
| void _addDefaultSuperConstructorDeclaration(ClassDeclaration class_) { |
| var classElement = class_.declaredElement; |
| var supertype = classElement?.supertype; |
| if (supertype != null) { |
| var unnamedConstructor = |
| supertype.constructors.firstWhereOrNull((e) => e.name.isEmpty); |
| if (unnamedConstructor != null) { |
| _addDeclaration(unnamedConstructor); |
| } |
| } |
| } |
| |
| void _addNamedType(NamedType? node) { |
| if (node == null) { |
| return; |
| } |
| |
| var element = node.element; |
| if (element == null) { |
| return; |
| } |
| |
| var declaration = declarationMap[element]; |
| if (declaration == null) { |
| return; |
| } |
| |
| declarations.add(declaration); |
| } |
| |
| void _addNamedTypes(List<NamedType>? nodes) { |
| nodes?.forEach(_addNamedType); |
| } |
| |
| void _visitCompoundAssignmentExpression(CompoundAssignmentExpression node) { |
| var readElement = node.readElement; |
| if (readElement != null) { |
| _addDeclaration(readElement); |
| } |
| var writeElement = node.writeElement; |
| if (writeElement != null) { |
| _addDeclaration(writeElement); |
| } |
| } |
| } |
| |
| class _Visitor extends SimpleAstVisitor<void> { |
| final LintRule rule; |
| final LinterContext context; |
| |
| _Visitor(this.rule, this.context); |
| |
| @override |
| void visitCompilationUnit(CompilationUnit node) { |
| var declarationGatherer = _DeclarationGatherer(linterContext: context); |
| for (var unit in context.allUnits) { |
| declarationGatherer.addDeclarations(unit.unit); |
| } |
| var declarations = declarationGatherer.declarations; |
| var entryPoints = declarations.where(_isEntryPoint); |
| if (entryPoints.isEmpty) return; |
| |
| // Map each top-level and static element to its declaration. |
| var declarationByElement = <Element, Declaration>{}; |
| for (var declaration in declarations) { |
| var element = declaration.declaredElement; |
| if (element != null) { |
| declarationByElement[element] = declaration; |
| if (element is TopLevelVariableElement) { |
| var getter = element.getter; |
| if (getter != null) declarationByElement[getter] = declaration; |
| var setter = element.setter; |
| if (setter != null) declarationByElement[setter] = declaration; |
| } else if (element is FieldElement) { |
| var getter = element.getter; |
| if (getter != null) declarationByElement[getter] = declaration; |
| var setter = element.setter; |
| if (setter != null) declarationByElement[setter] = declaration; |
| } |
| } |
| } |
| |
| // The set of the declarations which each top-level and static declaration |
| // references. |
| var dependencies = <Declaration, Set<Declaration>>{}; |
| |
| // Map each declaration to the collection of declarations which are |
| // referenced within its body. |
| for (var declaration in declarations) { |
| var visitor = _ReferenceVisitor(declarationByElement); |
| declaration.accept(visitor); |
| dependencies[declaration] = visitor.declarations; |
| } |
| |
| var usedMembers = entryPoints.toSet(); |
| var declarationsToCheck = Queue.of(usedMembers); |
| |
| // Loop through declarations which are reachable from the set of |
| // entry-points. We mark each such declaration as "used", and add its |
| // dependencies to the queue to loop through. Once the queue is empty, |
| // `usedMembers` contains every declaration reachable from an entry-point. |
| while (declarationsToCheck.isNotEmpty) { |
| var declaration = declarationsToCheck.removeLast(); |
| for (var dep in dependencies[declaration]!) { |
| if (usedMembers.add(dep)) { |
| declarationsToCheck.add(dep); |
| } |
| } |
| } |
| |
| var unitDeclarationGatherer = _DeclarationGatherer(linterContext: context); |
| unitDeclarationGatherer.addDeclarations(node); |
| var unitDeclarations = unitDeclarationGatherer.declarations; |
| var unusedDeclarations = unitDeclarations.difference(usedMembers); |
| var unusedMembers = unusedDeclarations.where((declaration) { |
| var element = declaration.declaredElement; |
| return element != null && |
| element.isPublic && |
| !element.hasVisibleForTesting; |
| }).toList(); |
| |
| for (var member in unusedMembers) { |
| if (member is ConstructorDeclaration) { |
| if (member.name == null) { |
| rule.reportLint(member.returnType, arguments: [member.nameForError]); |
| } else { |
| rule.reportLintForToken(member.name, |
| arguments: [member.nameForError]); |
| } |
| } else if (member is NamedCompilationUnitMember) { |
| rule.reportLintForToken(member.name, arguments: [member.nameForError]); |
| } else if (member is MethodDeclaration) { |
| rule.reportLintForToken(member.name, arguments: [member.name.lexeme]); |
| } else if (member is VariableDeclaration) { |
| rule.reportLintForToken(member.name, arguments: [member.nameForError]); |
| } else if (member is ExtensionDeclaration) { |
| var name = member.name; |
| rule.reportLintForToken( |
| name ?? member.extensionKeyword, |
| arguments: [name?.lexeme ?? '<unnamed>'], |
| ); |
| } else { |
| throw UnimplementedError('(${member.runtimeType}) $member'); |
| } |
| } |
| } |
| |
| 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'; |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| extension on Declaration { |
| String get nameForError { |
| // TODO(srawlins): Move this to analyzer when other uses are found. |
| var self = this; |
| if (self is ConstructorDeclaration) { |
| var name = self.name?.lexeme ?? 'new'; |
| return '${self.returnType.name}.$name'; |
| } else if (self is EnumConstantDeclaration) { |
| return self.name.lexeme; |
| } else if (self is ExtensionDeclaration) { |
| var name = self.name; |
| return name?.lexeme ?? 'the unnamed extension'; |
| } else if (self is MethodDeclaration) { |
| return self.name.lexeme; |
| } else if (self is NamedCompilationUnitMember) { |
| return self.name.lexeme; |
| } else if (self is VariableDeclaration) { |
| return self.name.lexeme; |
| } |
| |
| assert(false, 'Uncovered Declaration subtype: ${self.runtimeType}'); |
| return ''; |
| } |
| } |
| |
| extension on NamedType { |
| bool get isInExternalVariableTypeOrFunctionReturnType { |
| var topTypeAnnotation = topmostTypeAnnotation; |
| |
| switch (topTypeAnnotation.parent) { |
| case MethodDeclaration(:var externalKeyword, :var returnType): |
| return externalKeyword != null && returnType == topTypeAnnotation; |
| case VariableDeclarationList( |
| parent: FieldDeclaration(:var externalKeyword), |
| ): |
| case VariableDeclarationList( |
| parent: TopLevelVariableDeclaration(:var externalKeyword), |
| ): |
| return externalKeyword != null; |
| } |
| return false; |
| } |
| |
| TypeAnnotation get topmostTypeAnnotation { |
| TypeAnnotation topTypeAnnotation = this; |
| var parent = this.parent; |
| while (parent is TypeAnnotation) { |
| topTypeAnnotation = parent; |
| parent = topTypeAnnotation.parent; |
| } |
| return topTypeAnnotation; |
| } |
| } |