| // 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 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| |
| import '../analyzer.dart'; |
| import '../ast.dart'; |
| import '../extensions.dart'; |
| |
| const _desc = r'Use enums rather than classes that behave like enums.'; |
| |
| class UseEnums extends LintRule { |
| UseEnums() : super(name: LintNames.use_enums, description: _desc); |
| |
| @override |
| LintCode get lintCode => LinterLintCode.use_enums; |
| |
| @override |
| void registerNodeProcessors( |
| NodeLintRegistry registry, |
| LinterContext context, |
| ) { |
| if (!context.isEnabled(Feature.enhanced_enums)) return; |
| |
| var visitor = _Visitor(this, context); |
| registry.addClassDeclaration(this, visitor); |
| } |
| } |
| |
| /// A superclass for the [_EnumVisitor] and [_NonEnumVisitor]. |
| class _BaseVisitor extends RecursiveAstVisitor<void> { |
| /// The element representing the enum declaration that's being visited. |
| final ClassElement classElement; |
| |
| _BaseVisitor(this.classElement); |
| |
| /// Return `true` if the given [node] is an invocation of a generative |
| /// constructor from the class being converted. |
| bool invokesGenerativeConstructor(InstanceCreationExpression node) { |
| var constructorElement = node.constructorName.element; |
| return constructorElement != null && |
| !constructorElement.isFactory && |
| constructorElement.enclosingElement2 == classElement; |
| } |
| } |
| |
| /// A visitor used to visit the class being linted. This visitor throws an |
| /// exception if a constructor for the class is invoked anywhere other than the |
| /// top-level expression of an initializer for one of the fields being converted. |
| /// |
| /// Note: copied and modified from server. |
| class _EnumVisitor extends _BaseVisitor { |
| /// A flag indicating whether we are currently visiting the children of a |
| /// field declaration that will be converted to be a constant. |
| bool inConstantDeclaration = false; |
| |
| List<VariableDeclaration> variableDeclarations; |
| |
| _EnumVisitor(super.classElement, this.variableDeclarations); |
| |
| @override |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { |
| if (!inConstantDeclaration) { |
| if (invokesGenerativeConstructor(node)) { |
| throw _InvalidEnumException(); |
| } |
| } |
| inConstantDeclaration = false; |
| super.visitInstanceCreationExpression(node); |
| } |
| |
| @override |
| void visitVariableDeclaration(VariableDeclaration node) { |
| if (variableDeclarations.contains(node)) { |
| inConstantDeclaration = true; |
| } |
| super.visitVariableDeclaration(node); |
| inConstantDeclaration = false; |
| } |
| } |
| |
| /// Note: copied and modified from server. |
| class _InvalidEnumException implements Exception {} |
| |
| /// A visitor that visits everything in the library other than the class being |
| /// linted. This visitor throws an exception if |
| /// - there is a subclass of the class, or |
| /// - there is an invocation of one of the constructors of the class. |
| /// |
| /// Note: copied and modified from server. |
| class _NonEnumVisitor extends _BaseVisitor { |
| /// Initialize a newly created visitor to visit everything except the class |
| /// declaration corresponding to the given [classElement]. |
| _NonEnumVisitor(super.classElement); |
| |
| @override |
| void visitClassDeclaration(ClassDeclaration node) { |
| var element = node.declaredFragment?.element; |
| if (element == null) { |
| throw _InvalidEnumException(); |
| } |
| if (element != classElement) { |
| if (element.supertype?.element3 == classElement) { |
| throw _InvalidEnumException(); |
| } else if (element.interfaces |
| .map((e) => e.element3) |
| .contains(classElement)) { |
| throw _InvalidEnumException(); |
| } else if (element.mixins.map((e) => e.element3).contains(classElement)) { |
| // This case won't occur unless there's an error in the source code, but |
| // it's easier to check for the condition than it is to check for the |
| // diagnostic. |
| throw _InvalidEnumException(); |
| } |
| super.visitClassDeclaration(node); |
| } |
| } |
| |
| @override |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { |
| if (invokesGenerativeConstructor(node)) { |
| throw _InvalidEnumException(); |
| } |
| super.visitInstanceCreationExpression(node); |
| } |
| } |
| |
| class _Visitor extends SimpleAstVisitor<void> { |
| final LintRule rule; |
| final LinterContext context; |
| |
| _Visitor(this.rule, this.context); |
| |
| @override |
| visitClassDeclaration(ClassDeclaration node) { |
| // Don't lint augmentations. |
| if (node.isAugmentation) return; |
| |
| if (node.abstractKeyword != null) return; |
| var classElement = node.declaredFragment?.element; |
| if (classElement == null) return; |
| |
| // Enums can only extend Object. |
| if (classElement.supertype?.isDartCoreObject == false) { |
| return; |
| } |
| |
| var candidateConstants = <VariableDeclaration>[]; |
| |
| for (var member in node.members) { |
| if (isHashCode(member)) return; |
| if (isIndex(member)) return; |
| if (isEquals(member)) return; |
| if (isValues(member)) return; |
| |
| if (member is FieldDeclaration) { |
| if (!member.isStatic) continue; |
| for (var field in member.fields.variables) { |
| var fieldElement = field.declaredFragment?.element; |
| if (fieldElement is! FieldElement) continue; |
| if (field.isSynthetic || !field.isConst) continue; |
| var initializer = field.initializer; |
| if (initializer is! InstanceCreationExpression) continue; |
| |
| var constructorElement = initializer.constructorName.element; |
| if (constructorElement == null) continue; |
| if (constructorElement.isFactory) continue; |
| if (constructorElement.enclosingElement2 != classElement) continue; |
| if (fieldElement.computeConstantValue() == null) continue; |
| |
| candidateConstants.add(field); |
| } |
| } |
| if (member is ConstructorDeclaration) { |
| var constructor = member.declaredFragment?.element; |
| if (constructor == null) return; |
| if (!constructor.isFactory && !constructor.isConst) return; |
| var name = member.name?.lexeme; |
| if (classElement.isPublic && |
| (name == null || !Identifier.isPrivateName(name))) { |
| return; |
| } |
| } |
| } |
| |
| if (candidateConstants.length < 2) return; |
| |
| try { |
| node.accept(_EnumVisitor(classElement, candidateConstants)); |
| node.root.accept(_NonEnumVisitor(classElement)); |
| } on _InvalidEnumException { |
| return; |
| } |
| |
| rule.reportAtToken(node.name); |
| } |
| } |