blob: 96679679409c116e2772b4a48d29fd9577d0881f [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 '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';
const _desc = r'Use enums rather than classes that behave like enums.';
// todo(pq): add a link to any docs describing enhanced-enums once published
const _details = r'''
Classes that look like enumerations should be declared as `enum`s.
**DO** use enums where appropriate.
Candidates for enums are classes that:
* are concrete,
* are private or have only private generative constructors,
* have two or more static const fields with the same type as the class,
* have generative constructors that are only invoked at the top-level of the
initialization expression of these static fields,
* do not define `hashCode`, `==`, `values` or `index`,
* do not extend any class other than Object, and
* have no subclasses declared in the defining library.
**BAD:**
```dart
class LogPriority {
static const error = LogPriority._(1, 'Error');
static const warning = LogPriority._(2, 'Warning');
static const log = LogPriority._unknown('Log');
final String prefix;
final int priority;
const LogPriority._(this.priority, this.prefix);
const LogPriority._unknown(String prefix) : this._(-1, prefix);
}
```
**GOOD:**
```dart
enum LogPriority {
error(1, 'Error'),
warning(2, 'Warning'),
log.unknown('Log');
final String prefix;
final int priority;
const LogPriority(this.priority, this.prefix);
const LogPriority.unknown(String prefix) : this(-1, prefix);
}
```
''';
class UseEnums extends LintRule {
UseEnums()
: super(
name: 'use_enums',
description: _desc,
details: _details,
maturity: Maturity.experimental,
group: Group.style);
@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.staticElement;
return constructorElement != null &&
!constructorElement.isFactory &&
constructorElement.enclosingElement == 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.declaredElement;
if (element == null) {
throw _InvalidEnumException();
}
if (element != classElement) {
if (element.supertype?.element == classElement) {
throw _InvalidEnumException();
} else if (element.interfaces
.map((e) => e.element)
.contains(classElement)) {
throw _InvalidEnumException();
} else if (element.mixins.map((e) => e.element).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 {
final LintRule rule;
final LinterContext context;
_Visitor(this.rule, this.context);
@override
visitClassDeclaration(ClassDeclaration node) {
if (node.abstractKeyword != null) return;
var classElement = node.declaredElement;
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.declaredElement;
if (fieldElement is! FieldElement) continue;
if (field.isSynthetic || !field.isConst) continue;
var initializer = field.initializer;
if (initializer is! InstanceCreationExpression) continue;
var constructorElement = initializer.constructorName.staticElement;
if (constructorElement == null) continue;
if (constructorElement.isFactory) continue;
if (constructorElement.enclosingElement != classElement) continue;
if (fieldElement.computeConstantValue() == null) continue;
candidateConstants.add(field);
}
}
if (member is ConstructorDeclaration) {
var constructor = member.declaredElement;
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.reportLintForToken(node.name);
}
}