Add a quick assist to convert a class to an enum
Change-Id: Iebd83940c85e778132132bfe483df08193f574ff
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/233002
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/assist.dart b/pkg/analysis_server/lib/src/services/correction/assist.dart
index 3f8be5c..0e0b1c9 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist.dart
@@ -56,6 +56,11 @@
30,
'Assign value to new local variable',
);
+ static const CONVERT_CLASS_TO_ENUM = AssistKind(
+ 'dart.assist.convert.classToEnum',
+ 30,
+ 'Convert class to an enum',
+ );
static const CONVERT_CLASS_TO_MIXIN = AssistKind(
'dart.assist.convert.classToMixin',
30,
diff --git a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
index 3cbb3f8..70835d5 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
@@ -12,6 +12,7 @@
import 'package:analysis_server/src/services/correction/dart/add_type_annotation.dart';
import 'package:analysis_server/src/services/correction/dart/assign_to_local_variable.dart';
import 'package:analysis_server/src/services/correction/dart/convert_add_all_to_spread.dart';
+import 'package:analysis_server/src/services/correction/dart/convert_class_to_enum.dart';
import 'package:analysis_server/src/services/correction/dart/convert_class_to_mixin.dart';
import 'package:analysis_server/src/services/correction/dart/convert_conditional_expression_to_if_element.dart';
import 'package:analysis_server/src/services/correction/dart/convert_documentation_into_block.dart';
@@ -91,6 +92,7 @@
AddTypeAnnotation.newInstanceBulkFixable,
AssignToLocalVariable.newInstance,
ConvertAddAllToSpread.newInstance,
+ ConvertClassToEnum.newInstance,
ConvertClassToMixin.newInstance,
ConvertConditionalExpressionToIfElement.newInstance,
ConvertDocumentationIntoBlock.newInstance,
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/convert_class_to_enum.dart b/pkg/analysis_server/lib/src/services/correction/dart/convert_class_to_enum.dart
new file mode 100644
index 0000000..978a6f5
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/convert_class_to_enum.dart
@@ -0,0 +1,768 @@
+// 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:analysis_server/src/services/correction/assist.dart';
+import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
+import 'package:analysis_server/src/services/correction/util.dart';
+import 'package:analysis_server/src/utilities/extensions/range_factory.dart';
+import 'package:analyzer/dart/analysis/features.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/constant/value.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer_plugin/utilities/assist/assist.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+import 'package:collection/collection.dart';
+
+class ConvertClassToEnum extends CorrectionProducer {
+ @override
+ AssistKind get assistKind => DartAssistKind.CONVERT_CLASS_TO_ENUM;
+
+ @override
+ Future<void> compute(ChangeBuilder builder) async {
+ if (!libraryElement.featureSet.isEnabled(Feature.enhanced_enums)) {
+ // If the library doesn't support enhanced_enums then the class can't be
+ // converted.
+ return;
+ }
+ if (libraryElement.units.length > 1) {
+ // If the library has any part files, then the class can't be converted
+ // because we don't currently have a performant way to access the ASTs for
+ // the parts to check for invocations of the constructors or subclasses of
+ // the class.
+ return;
+ }
+ var node = this.node;
+ if (node is! SimpleIdentifier) {
+ return;
+ }
+ var parent = node.parent;
+ if (parent is ClassDeclaration && parent.name == node) {
+ var description = _EnumDescription.fromClass(parent);
+ if (description != null) {
+ await builder.addDartFileEdit(file, (builder) {
+ description.applyChanges(builder, utils);
+ });
+ }
+ }
+ }
+
+ /// Return an instance of this class. Used as a tear-off in `AssistProcessor`.
+ static ConvertClassToEnum newInstance() => ConvertClassToEnum();
+}
+
+/// 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 constructor from
+ /// the class being converted.
+ bool invokesEnumConstructor(InstanceCreationExpression node) {
+ var constructorElement = node.constructorName.staticElement;
+ return constructorElement?.enclosingElement == classElement;
+ }
+}
+
+/// An exception thrown by the visitors if a condition is found that prevents
+/// the class from being converted.
+class _CannotConvertException implements Exception {
+ final String message;
+
+ _CannotConvertException(this.message);
+}
+
+/// A representation of a static field in the class being converted that will be
+/// replaced by an enum constant.
+class _ConstantField extends _Field {
+ /// The element representing the constructor used to initialize the field.
+ ConstructorElement constructorElement;
+
+ /// The invocation of the constructor.
+ final InstanceCreationExpression instanceCreation;
+
+ /// The value of the index field.
+ final int indexValue;
+
+ _ConstantField(
+ FieldElement element,
+ VariableDeclaration declaration,
+ VariableDeclarationList declarationList,
+ FieldDeclaration fieldDeclaration,
+ this.instanceCreation,
+ this.constructorElement,
+ this.indexValue)
+ : super(element, declaration, declarationList, fieldDeclaration);
+}
+
+/// Information about a single constructor in the class being converted.
+class _Constructor {
+ /// The declaration of the constructor.
+ final ConstructorDeclaration declaration;
+
+ /// The element representing the constructor.
+ final ConstructorElement element;
+
+ _Constructor(this.declaration, this.element);
+}
+
+/// Information about the constructors in the class being converted.
+class _Constructors {
+ /// A map from elements to constructors.
+ final Map<ConstructorElement, _Constructor> byElement = {};
+
+ _Constructors();
+
+ /// Return the constructors in this collection.
+ Iterable<_Constructor> get constructors => byElement.values;
+
+ /// Add the given [constructor] to this collection.
+ void add(_Constructor constructor) {
+ byElement[constructor.element] = constructor;
+ }
+
+ /// Return the constructor with the given [element].
+ _Constructor? forElement(ConstructorElement element) {
+ return byElement[element];
+ }
+}
+
+/// A description of how to convert the class to an enum.
+class _EnumDescription {
+ /// The class declaration being converted.
+ final ClassDeclaration classDeclaration;
+
+ /// A map from constructor declarations to information about the parameter
+ /// corresponding to the index field. The map is `null` if there is no index
+ /// field.
+ final Map<_Constructor, _Parameter>? constructorMap;
+
+ /// A list of the declarations to be converted into enum constants.
+ final _Fields fields;
+
+ /// A list of the indexes of members that need to be deleted.
+ final List<int> membersToDelete;
+
+ _EnumDescription({
+ required this.classDeclaration,
+ required this.constructorMap,
+ required this.fields,
+ required this.membersToDelete,
+ });
+
+ /// Return the offset immediately following the opening brace for the class
+ /// body.
+ int get bodyOffset => classDeclaration.leftBracket.end;
+
+ /// Use the [builder] and correction [utils] to apply the change necessary to
+ /// convert the class to an enum.
+ void applyChanges(DartFileEditBuilder builder, CorrectionUtils utils) {
+ // Replace the keyword.
+ builder.addSimpleReplacement(
+ range.token(classDeclaration.classKeyword), 'enum');
+
+ // Remove the extends clause if there is one.
+ final extendsClause = classDeclaration.extendsClause;
+ if (extendsClause != null) {
+ var followingToken = extendsClause.endToken.next!;
+ builder.addDeletion(range.startStart(extendsClause, followingToken));
+ }
+
+ // Compute the declarations of the enum constants and delete the fields
+ // being converted.
+ var members = classDeclaration.members;
+ var indent = utils.getIndent(1);
+ var constantsBuffer = StringBuffer();
+ var fieldsToConvert = fields.fieldsToConvert;
+ fieldsToConvert
+ .sort((first, second) => first.indexValue.compareTo(second.indexValue));
+ for (var field in fieldsToConvert) {
+ // Compute the declaration of the corresponding enum constant.
+ if (constantsBuffer.isNotEmpty) {
+ constantsBuffer.write(',\n$indent');
+ }
+ constantsBuffer.write(field.name);
+ var invocation = field.instanceCreation;
+ var constructorNameNode = invocation.constructorName;
+ var invokedConstructorElement = field.constructorElement;
+ var invokedConstructor = constructorMap?.keys.firstWhere(
+ (constructor) => constructor.element == invokedConstructorElement);
+ var parameterData = constructorMap?[invokedConstructor];
+ var typeArguments = constructorNameNode.type.typeArguments;
+ if (typeArguments != null) {
+ constantsBuffer.write(utils.getNodeText(typeArguments));
+ }
+ var constructorName = constructorNameNode.name?.name;
+ if (constructorName != null) {
+ constantsBuffer.write('.$constructorName');
+ }
+ var argumentList = invocation.argumentList;
+ var arguments = argumentList.arguments;
+ var argumentCount = arguments.length - (parameterData == null ? 0 : 1);
+ if (argumentCount == 0) {
+ if (typeArguments != null || constructorName != null) {
+ constantsBuffer.write('()');
+ }
+ } else if (parameterData == null) {
+ constantsBuffer.write(utils.getNodeText(argumentList));
+ } else {
+ constantsBuffer.write('(');
+ var index = parameterData.index;
+ var last = arguments.length - 1;
+ if (index == 0) {
+ var offset = arguments[1].offset;
+ var length = arguments[last].end - offset;
+ constantsBuffer.write(utils.getText(offset, length));
+ } else if (index == last) {
+ var offset = arguments[0].offset;
+ int length;
+ if (arguments[last].endToken.next?.type == TokenType.COMMA) {
+ length = arguments[last].offset - offset;
+ } else {
+ length = arguments[last - 1].end - offset;
+ }
+ constantsBuffer.write(utils.getText(offset, length));
+ } else {
+ var offset = arguments[0].offset;
+ var length = arguments[index].offset - offset;
+ constantsBuffer.write(utils.getText(offset, length));
+
+ offset = arguments[index + 1].offset;
+ length = argumentList.endToken.offset - offset;
+ constantsBuffer.write(utils.getText(offset, length));
+ }
+ constantsBuffer.write(')');
+ }
+
+ // Delete the static field that was converted to an enum constant.
+ _deleteField(builder, field, members);
+ }
+
+ // Remove the index field.
+ var indexField = fields.indexField;
+ if (indexField != null) {
+ _deleteField(builder, indexField, members);
+ }
+
+ // Update the constructors.
+ var removedConstructor = _removeUnnamedConstructor();
+ _transformConstructors(builder, removedConstructor);
+
+ // Special case replacing all of the members.
+ if (membersToDelete.length == members.length) {
+ builder.addSimpleReplacement(range.startEnd(members.first, members.last),
+ constantsBuffer.toString());
+ return;
+ }
+
+ // Insert the declarations of the enum constants.
+ var semicolon = ';';
+ var prefix = '${utils.endOfLine}$indent';
+ var suffix = '$semicolon${utils.endOfLine}';
+ builder.addSimpleInsertion(bodyOffset, '$prefix$constantsBuffer$suffix');
+
+ // Delete any members that are no longer needed.
+ membersToDelete.sort();
+ for (var range in range.nodesInList(members, membersToDelete)) {
+ builder.addDeletion(range);
+ }
+ }
+
+ /// Use the [builder] to delete the [field].
+ void _deleteField(DartFileEditBuilder builder, _Field field,
+ NodeList<ClassMember> members) {
+ var variableList = field.declarationList;
+ if (variableList.variables.length == 1) {
+ membersToDelete.add(members.indexOf(field.fieldDeclaration));
+ } else {
+ builder.addDeletion(
+ range.nodeInList(variableList.variables, field.declaration));
+ }
+ }
+
+ /// If the unnamed constructor is the only constructor, and if it has no
+ /// parameters other than potentially the index field, then remove it.
+ ConstructorDeclaration? _removeUnnamedConstructor() {
+ var members = classDeclaration.members;
+ var constructors = members.whereType<ConstructorDeclaration>().toList();
+ if (constructors.length != 1) {
+ return null;
+ }
+ var constructor = constructors[0];
+ var name = constructor.name?.name;
+ if (name != null && name != 'new') {
+ return null;
+ }
+ var parameters = constructor.parameters.parameters;
+ // If there's only one constructor, then there can only be one entry in the
+ // constructor map.
+ var parameterData = constructorMap?.entries.first.value;
+ // `parameterData` should only be `null` if there is no index field.
+ var updatedParameterCount =
+ parameters.length - (parameterData == null ? 0 : 1);
+ if (updatedParameterCount != 0) {
+ return null;
+ }
+ membersToDelete.add(members.indexOf(constructor));
+ return constructor;
+ }
+
+ /// Transform the used constructors by removing the parameter corresponding to
+ /// the index field.
+ void _transformConstructors(
+ DartFileEditBuilder builder, ConstructorDeclaration? removedConstructor) {
+ final constructorMap = this.constructorMap;
+ if (constructorMap == null) {
+ return;
+ }
+ for (var constructor in constructorMap.keys) {
+ if (constructor.declaration != removedConstructor) {
+ var parameterData = constructorMap[constructor];
+ if (parameterData != null) {
+ var parameters = constructor.declaration.parameters.parameters;
+ builder.addDeletion(
+ range.nodeInList(parameters, parameters[parameterData.index]));
+ }
+ }
+ }
+ }
+
+ /// If the given [node] can be converted into an enum, then return a
+ /// description of the conversion work to be done. Otherwise, return `null`.
+ static _EnumDescription? fromClass(ClassDeclaration node) {
+ // The class must be a concrete class.
+ var classElement = node.declaredElement;
+ if (classElement == null || classElement.isAbstract) {
+ return null;
+ }
+
+ // The class must be a subclass of Object, whether implicitly or explicitly.
+ var extendsClause = node.extendsClause;
+ if (extendsClause != null &&
+ extendsClause.superclass.type?.isDartCoreObject == false) {
+ return null;
+ }
+
+ // The class must either be private or must only have private constructors.
+ var constructors = _validateConstructors(node, classElement);
+ if (constructors == null) {
+ return null;
+ }
+
+ // The class must not override either `==` or `hashCode`.
+ if (!_validateMethods(node)) {
+ return null;
+ }
+
+ // There must be at least one static field that can be converted into an
+ // enum constant.
+ //
+ // The instance fields must all be final.
+ var fields = _validateFields(node, classElement);
+ if (fields == null || fields.fieldsToConvert.isEmpty) {
+ return null;
+ }
+
+ var visitor = _EnumVisitor(classElement, fields.fieldsToConvert);
+ try {
+ node.accept(visitor);
+ } on _CannotConvertException {
+ return null;
+ }
+
+ // Within the defining library,
+ // - there can't be any subclasses of the class to be converted,
+ // - there can't be any invocations of any constructor from that class.
+ try {
+ node.root.accept(_NonEnumVisitor(classElement));
+ } on _CannotConvertException {
+ return null;
+ }
+
+ var usedConstructors = _computeUsedConstructors(constructors, fields);
+ var constructorMap = _indexFieldData(usedConstructors, fields);
+ if (fields.indexField != null && constructorMap == null) {
+ return null;
+ }
+
+ var membersToDelete = <int>[];
+ return _EnumDescription(
+ classDeclaration: node,
+ constructorMap: constructorMap,
+ fields: fields,
+ membersToDelete: membersToDelete,
+ );
+ }
+
+ /// Return the subset of [constructors] that are invoked by the [fields] to be
+ /// converted.
+ static _Constructors _computeUsedConstructors(
+ _Constructors constructors, _Fields fields) {
+ var usedElements = <ConstructorElement>{};
+ for (var field in fields.fieldsToConvert) {
+ usedElements.add(field.constructorElement);
+ }
+ var usedConstructors = _Constructors();
+ for (var element in usedElements) {
+ var constructor = constructors.forElement(element);
+ if (constructor != null) {
+ usedConstructors.add(constructor);
+ }
+ }
+ return usedConstructors;
+ }
+
+ /// If the index field can be removed, return a map describing the changes
+ /// that need to be made to both the constructors and the invocations of those
+ /// constructors. Otherwise, return `null`.
+ static Map<_Constructor, _Parameter>? _indexFieldData(
+ _Constructors usedConstructors, _Fields fields) {
+ var indexField = fields.indexField;
+ if (indexField == null) {
+ return null;
+ }
+ // Ensure that the index field has a corresponding field formal initializer
+ // in each of the used constructors.
+ var constructorMap = <_Constructor, _Parameter>{};
+ for (var constructor in usedConstructors.constructors) {
+ var parameterData = _indexParameter(constructor, indexField);
+ if (parameterData == null) {
+ return null;
+ }
+ constructorMap[constructor] = parameterData;
+ }
+
+ var fieldsToConvert = fields.fieldsToConvert;
+ var values = <int>{};
+ for (var field in fieldsToConvert) {
+ var constructorElement = field.constructorElement;
+ var constructor = usedConstructors.forElement(constructorElement);
+ if (constructor == null) {
+ // We should never reach this point.
+ return null;
+ }
+ var parameterData = constructorMap[constructor];
+ if (parameterData == null) {
+ // We should never reach this point.
+ return null;
+ }
+ var arguments = field.instanceCreation.argumentList.arguments;
+ var argument = parameterData.getArgument(arguments);
+ if (argument is! IntegerLiteral) {
+ return null;
+ }
+ var value = argument.value;
+ if (value == null) {
+ return null;
+ }
+ if (!values.add(value)) {
+ // Duplicate value.
+ return null;
+ }
+ }
+ var sortedValues = values.toList()..sort();
+ if (sortedValues.length == fieldsToConvert.length &&
+ sortedValues.first == 0 &&
+ sortedValues.last == fieldsToConvert.length - 1) {
+ return constructorMap;
+ }
+ return null;
+ }
+
+ static _Parameter? _indexParameter(
+ _Constructor constructor, _Field? indexField) {
+ if (indexField == null) {
+ return null;
+ }
+ var parameters = constructor.declaration.parameters.parameters;
+ var indexFieldElement = indexField.element;
+ for (var i = 0; i < parameters.length; i++) {
+ var element = parameters[i].declaredElement;
+ if (element is FieldFormalParameterElement) {
+ if (element.field == indexFieldElement) {
+ if (element.isPositional) {
+ return _Parameter(i, element);
+ } else {
+ return _Parameter(i, element);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /// Return a representation of all of the constructors declared by the
+ /// [classDeclaration], or `null` if the class can't be converted.
+ ///
+ /// The [classElement] must be the element declared by the [classDeclaration].
+ static _Constructors? _validateConstructors(
+ ClassDeclaration classDeclaration, ClassElement classElement) {
+ var constructors = _Constructors();
+ for (var member in classDeclaration.members) {
+ if (member is ConstructorDeclaration) {
+ var constructor = member.declaredElement;
+ if (constructor is ConstructorElement) {
+ if (!classElement.isPrivate && !constructor.isPrivate) {
+ // Public constructor in public enum.
+ return null;
+ } else if (constructor.isFactory) {
+ // Factory constructor.
+ return null;
+ } else if (!constructor.isConst) {
+ // Non-const constructor.
+ return null;
+ }
+ constructors.add(_Constructor(member, constructor));
+ } else {
+ // Not resolved.
+ return null;
+ }
+ }
+ }
+ return constructors;
+ }
+
+ /// Return a representation of all of the constructors declared by the
+ /// [classDeclaration], or `null` if the class can't be converted.
+ ///
+ /// The [classElement] must be the element declared by the [classDeclaration].
+ static _Fields? _validateFields(
+ ClassDeclaration classDeclaration, ClassElement classElement) {
+ var potentialFieldsToConvert = <DartObject, List<_ConstantField>>{};
+ _Field? indexField;
+
+ for (var member in classDeclaration.members) {
+ if (member is FieldDeclaration) {
+ var fieldList = member.fields;
+ var fields = fieldList.variables;
+ if (member.isStatic) {
+ for (var field in fields) {
+ var fieldElement = field.declaredElement;
+ if (fieldElement is FieldElement) {
+ var fieldType = fieldElement.type;
+ // The field can be converted to be an enum constant if it
+ // - is a const field,
+ // - has a type equal to the type of the class, and
+ // - is initialized by an instance creation expression defined in this
+ // class.
+ if (fieldElement.isConst &&
+ fieldType is InterfaceType &&
+ fieldType.element == classElement) {
+ var initializer = field.initializer;
+ if (initializer is InstanceCreationExpression) {
+ var constructorElement =
+ initializer.constructorName.staticElement;
+ if (constructorElement != null &&
+ constructorElement.enclosingElement == classElement) {
+ var fieldValue = fieldElement.computeConstantValue();
+ if (fieldValue != null) {
+ if (fieldList.variables.length != 1) {
+ // Too many constants in the field declaration.
+ return null;
+ }
+ potentialFieldsToConvert
+ .putIfAbsent(fieldValue, () => [])
+ .add(_ConstantField(
+ fieldElement,
+ field,
+ fieldList,
+ member,
+ initializer,
+ constructorElement,
+ fieldValue.getField('index')?.toIntValue() ??
+ -1));
+ }
+ }
+ }
+ }
+ }
+ }
+ } else {
+ for (var field in fields) {
+ if (!field.isFinal) {
+ // Non-final instance field.
+ return null;
+ }
+ var fieldElement = field.declaredElement;
+ if (fieldElement is FieldElement) {
+ var fieldType = fieldElement.type;
+ if (fieldElement.name == 'index' && fieldType.isDartCoreInt) {
+ indexField = _Field(fieldElement, field, fieldList, member);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ var fieldsToConvert = <_ConstantField>[];
+ for (var list in potentialFieldsToConvert.values) {
+ if (list.length == 1) {
+ fieldsToConvert.add(list[0]);
+ } else {
+ // TODO(brianwilkerson) We could potentially handle the case where
+ // there's only one non-deprecated field in the list. We'd need to
+ // change the return type for this method so that we could return two
+ // lists: the list of fields to convert and the list of fields whose
+ // initializer needs to be updated to refer to the constant.
+ return null;
+ }
+ }
+ return _Fields(fieldsToConvert, indexField);
+ }
+
+ /// Return `true` if the [classDeclaration] does not contain any methods that
+ /// prevent it from being converted.
+ static bool _validateMethods(ClassDeclaration classDeclaration) {
+ for (var member in classDeclaration.members) {
+ if (member is MethodDeclaration) {
+ if (member.name.name == '==' || member.name.name == 'hashCode') {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
+
+/// A visitor used to visit the class being converted. 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.
+class _EnumVisitor extends _BaseVisitor {
+ /// The declarations of the fields that are to be converted.
+ final List<VariableDeclaration> fieldsToConvert;
+
+ /// 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;
+
+ /// Initialize a newly created visitor to visit the class declaration
+ /// corresponding to the given [classElement].
+ _EnumVisitor(ClassElement classElement, List<_ConstantField> fieldsToConvert)
+ : fieldsToConvert =
+ fieldsToConvert.map((field) => field.declaration).toList(),
+ super(classElement);
+
+ @override
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
+ if (!inConstantDeclaration) {
+ if (invokesEnumConstructor(node)) {
+ throw _CannotConvertException(
+ 'Constructor used outside constant initializer');
+ }
+ }
+ inConstantDeclaration = false;
+ super.visitInstanceCreationExpression(node);
+ }
+
+ @override
+ void visitVariableDeclaration(VariableDeclaration node) {
+ if (fieldsToConvert.contains(node)) {
+ inConstantDeclaration = true;
+ }
+ super.visitVariableDeclaration(node);
+ inConstantDeclaration = false;
+ }
+}
+
+/// A representation of a field of interest in the class being converted.
+class _Field {
+ /// The element representing the field.
+ final FieldElement element;
+
+ /// The declaration of the field.
+ final VariableDeclaration declaration;
+
+ /// The list containing the [declaration]
+ final VariableDeclarationList declarationList;
+
+ /// The field declaration containing the [declarationList].
+ final FieldDeclaration fieldDeclaration;
+
+ _Field(this.element, this.declaration, this.declarationList,
+ this.fieldDeclaration);
+
+ /// Return the name of the field.
+ String get name => declaration.name.name;
+}
+
+/// A representation of all the fields of interest in the class being converted.
+class _Fields {
+ /// The fields to be converted into enum constants.
+ List<_ConstantField> fieldsToConvert;
+
+ /// The index field, or `null` if there is no index field.
+ _Field? indexField;
+
+ _Fields(this.fieldsToConvert, this.indexField);
+}
+
+/// A visitor that visits everything in the library other than the class being
+/// converted. This visitor throws an exception if the class can't be converted
+/// because
+/// - there is a subclass of the class, or
+/// - there is an invocation of one of the constructors of the class.
+class _NonEnumVisitor extends _BaseVisitor {
+ /// Initialize a newly created visitor to visit everything except the class
+ /// declaration corresponding to the given [classElement].
+ _NonEnumVisitor(ClassElement classElement) : super(classElement);
+
+ @override
+ void visitClassDeclaration(ClassDeclaration node) {
+ var element = node.declaredElement;
+ if (element == null) {
+ throw _CannotConvertException('Unresolved');
+ }
+ if (element != classElement) {
+ if (element.supertype?.element == classElement) {
+ throw _CannotConvertException('Class is extended');
+ } else if (element.interfaces
+ .map((e) => e.element)
+ .contains(classElement)) {
+ throw _CannotConvertException('Class is implemented');
+ } 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 _CannotConvertException('Class is mixed in');
+ }
+ super.visitClassDeclaration(node);
+ }
+ }
+
+ @override
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
+ if (invokesEnumConstructor(node)) {
+ throw _CannotConvertException(
+ 'Constructor used outside class being converted');
+ }
+ super.visitInstanceCreationExpression(node);
+ }
+}
+
+/// An object used to access information about a specific parameter, including
+/// its index in the parameter list as well as any associated argument in an
+/// argument list.
+class _Parameter {
+ /// The index of this parameter in the enclosing constructor's parameter list.
+ final int index;
+
+ /// The element associated with the parameter.
+ final ParameterElement element;
+
+ _Parameter(this.index, this.element);
+
+ /// Return the expression representing the argument associated with this
+ /// parameter, or `null` if there is no such argument.
+ Expression? getArgument(NodeList<Expression> arguments) {
+ return arguments.firstWhereOrNull(
+ (argument) => argument.staticParameterElement == element);
+ }
+}
diff --git a/pkg/analysis_server/lib/src/utilities/extensions/range_factory.dart b/pkg/analysis_server/lib/src/utilities/extensions/range_factory.dart
index 95c5384..eedc3c7 100644
--- a/pkg/analysis_server/lib/src/utilities/extensions/range_factory.dart
+++ b/pkg/analysis_server/lib/src/utilities/extensions/range_factory.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/scanner/token.dart';
+import 'package:analysis_server/src/utilities/index_range.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/source/source_range.dart';
@@ -67,6 +68,30 @@
}
}
+ /// Return a list of the ranges that cover all of the elements in the [list]
+ /// whose index is in the list of [indexes].
+ List<SourceRange> nodesInList<T extends AstNode>(
+ NodeList<T> list, List<int> indexes) {
+ var ranges = <SourceRange>[];
+ var indexRanges = IndexRange.contiguousSubRanges(indexes);
+ if (indexRanges.length == 1) {
+ var indexRange = indexRanges[0];
+ if (indexRange.lower == 0 && indexRange.upper == list.length - 1) {
+ ranges.add(startEnd(list[indexRange.lower], list[indexRange.upper]));
+ return ranges;
+ }
+ }
+ for (var indexRange in indexRanges) {
+ if (indexRange.lower == 0) {
+ ranges.add(
+ startStart(list[indexRange.lower], list[indexRange.upper + 1]));
+ } else {
+ ranges.add(endEnd(list[indexRange.lower - 1], list[indexRange.upper]));
+ }
+ }
+ return ranges;
+ }
+
/// Return a source range that covers the given [node] with any leading and
/// trailing comments.
///
diff --git a/pkg/analysis_server/lib/src/utilities/index_range.dart b/pkg/analysis_server/lib/src/utilities/index_range.dart
new file mode 100644
index 0000000..648ec02
--- /dev/null
+++ b/pkg/analysis_server/lib/src/utilities/index_range.dart
@@ -0,0 +1,42 @@
+// 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.
+
+/// A range of indexes within a list.
+class IndexRange {
+ /// The index of the first element in the range.
+ final int lower;
+
+ /// The index of the last element in the range. This will be the same as the
+ /// [lower] if there is a single element in the range.
+ final int upper;
+
+ /// Initialize a newly created range.
+ IndexRange(this.lower, this.upper);
+
+ /// Return the number of indices in this range.
+ int get count => upper - lower + 1;
+
+ @override
+ String toString() => '[$lower..$upper]';
+
+ static List<IndexRange> contiguousSubRanges(List<int> indexes) {
+ var ranges = <IndexRange>[];
+ if (indexes.isEmpty) {
+ return ranges;
+ }
+ var lower = indexes[0];
+ var previous = lower;
+ for (var index = 1; index < indexes.length; index++) {
+ var current = indexes[index];
+ if (current == previous + 1) {
+ previous = current;
+ } else {
+ ranges.add(IndexRange(lower, previous));
+ lower = previous = current;
+ }
+ }
+ ranges.add(IndexRange(lower, previous));
+ return ranges;
+ }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/assist/convert_class_to_enum_test.dart b/pkg/analysis_server/test/src/services/correction/assist/convert_class_to_enum_test.dart
new file mode 100644
index 0000000..931cc73
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/assist/convert_class_to_enum_test.dart
@@ -0,0 +1,516 @@
+// 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:analysis_server/src/services/correction/assist.dart';
+import 'package:analyzer_plugin/utilities/assist/assist.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'assist_processor.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(ConvertClassToEnumTest);
+ });
+}
+
+@reflectiveTest
+class ConvertClassToEnumTest extends AssistProcessorTest {
+ @override
+ AssistKind get kind => DartAssistKind.CONVERT_CLASS_TO_ENUM;
+
+ Future<void> test_extends_object_privateClass() async {
+ await resolveTestCode('''
+class _E extends Object {
+ static const _E c = _E();
+
+ const _E();
+}
+''');
+ await assertHasAssistAt('E extends', '''
+enum _E {
+ c
+}
+''');
+ }
+
+ Future<void> test_extends_object_publicClass() async {
+ await resolveTestCode('''
+class E extends Object {
+ static const E c = E._();
+
+ const E._();
+}
+''');
+ await assertHasAssistAt('E extends', '''
+enum E {
+ c._();
+
+ const E._();
+}
+''');
+ }
+
+ Future<void> test_index_namedIndex_first_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E(0, 'a');
+ static const _E c1 = _E(1, 'b');
+
+ final int index;
+
+ final String code;
+
+ const _E(this.index, this.code);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c0('a'),
+ c1('b');
+
+ final String code;
+
+ const _E(this.code);
+}
+''');
+ }
+
+ Future<void> test_index_namedIndex_last_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E('a', 0);
+ static const _E c1 = _E('b', 1);
+
+ final String code;
+
+ final int index;
+
+ const _E(this.code, this.index);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c0('a'),
+ c1('b');
+
+ final String code;
+
+ const _E(this.code);
+}
+''');
+ }
+
+ Future<void> test_index_namedIndex_middle_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E('a', 0, 'b');
+ static const _E c1 = _E('c', 1, 'd');
+
+ final String first;
+
+ final int index;
+
+ final String last;
+
+ const _E(this.first, this.index, this.last);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c0('a', 'b'),
+ c1('c', 'd');
+
+ final String first;
+
+ final String last;
+
+ const _E(this.first, this.last);
+}
+''');
+ }
+
+ Future<void> test_index_namedIndex_only_outOfOrder() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E(1);
+ static const _E c1 = _E(0);
+
+ final int index;
+
+ const _E(this.index);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c1,
+ c0
+}
+''');
+ }
+
+ Future<void> test_index_namedIndex_only_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E(0);
+ static const _E c1 = _E(1);
+
+ final int index;
+
+ const _E(this.index);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c0,
+ c1
+}
+''');
+ }
+
+ Future<void> test_index_namedIndex_only_publicClass() async {
+ await resolveTestCode('''
+class E {
+ static const E c0 = E._(0);
+ static const E c1 = E._(1);
+
+ final int index;
+
+ const E._(this.index);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum E {
+ c0._(),
+ c1._();
+
+ const E._();
+}
+''');
+ }
+
+ Future<void> test_index_notNamedIndex_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E(0);
+ static const _E c1 = _E(1);
+
+ final int value;
+
+ const _E(this.value);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c0(0),
+ c1(1);
+
+ final int value;
+
+ const _E(this.value);
+}
+''');
+ }
+
+ Future<void> test_index_notNamedIndex_publicClass() async {
+ await resolveTestCode('''
+class E {
+ static const E c0 = E._(0);
+ static const E c1 = E._(1);
+
+ final int value;
+
+ const E._(this.value);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum E {
+ c0._(0),
+ c1._(1);
+
+ final int value;
+
+ const E._(this.value);
+}
+''');
+ }
+
+ Future<void> test_invalid_abstractClass() async {
+ await resolveTestCode('''
+abstract class E {}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_constructorUsedInConstructor() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c = _E();
+
+ // ignore: unused_element
+ const _E({_E e = const _E()});
+}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_constructorUsedOutsideClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c = _E();
+
+ const _E();
+}
+_E get e => _E();
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_extended() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c = _E();
+
+ const _E();
+}
+class F extends _E {}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_extends_notObject() async {
+ await resolveTestCode('''
+class E extends C {
+ static const E c = E._();
+
+ const E._();
+}
+class C {
+ const C();
+}
+''');
+ await assertNoAssistAt('E extends');
+ }
+
+ Future<void> test_invalid_hasPart() async {
+ // Change this test if the assist becomes able to look for references to the
+ // class and its constructors in part files.
+ newFile('$testPackageLibPath/a.dart', content: '''
+part of 'test.dart';
+''');
+ await resolveTestCode('''
+part 'a.dart';
+
+class E {
+ static const E c = E._();
+
+ const E._();
+}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_implemented() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c = _E();
+
+ const _E();
+}
+class F implements _E {}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_indexFieldNotSequential() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E(0);
+ static const _E c1 = _E(3);
+
+ final int index;
+
+ const _E(this.index);
+}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_multipleConstantsInSameFieldDeclaration() async {
+ // Change this test if support is added to cover cases where multiple
+ // constants are defined in a single field declaration.
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E('a'), c1 = _E('b');
+
+ final String s;
+
+ const _E(this.s);
+}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_nonConstConstructor() async {
+ await resolveTestCode('''
+class _E {
+ static _E c = _E();
+
+ _E();
+}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_overrides_equal() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c = _E();
+
+ const _E();
+
+ @override
+ int get hashCode => 0;
+}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_invalid_overrides_hashCode() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c = _E();
+
+ const _E();
+
+ @override
+ bool operator ==(Object other) => true;
+}
+''');
+ await assertNoAssistAt('E {');
+ }
+
+ Future<void> test_minimal_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c = _E();
+
+ const _E();
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c
+}
+''');
+ }
+
+ Future<void> test_minimal_publicClass() async {
+ await resolveTestCode('''
+class E {
+ static const E c = E._();
+
+ const E._();
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum E {
+ c._();
+
+ const E._();
+}
+''');
+ }
+
+ Future<void> test_noIndex_int_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E(2);
+ static const _E c1 = _E(4);
+
+ final int count;
+
+ const _E(this.count);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c0(2),
+ c1(4);
+
+ final int count;
+
+ const _E(this.count);
+}
+''');
+ }
+
+ Future<void> test_noIndex_int_publicClass() async {
+ await resolveTestCode('''
+class E {
+ static const E c0 = E._(2);
+ static const E c1 = E._(4);
+
+ final int count;
+
+ const E._(this.count);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum E {
+ c0._(2),
+ c1._(4);
+
+ final int count;
+
+ const E._(this.count);
+}
+''');
+ }
+
+ Future<void> test_noIndex_notInt_privateClass() async {
+ await resolveTestCode('''
+class _E {
+ static const _E c0 = _E('c0');
+ static const _E c1 = _E('c1');
+
+ final String name;
+
+ const _E(this.name);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum _E {
+ c0('c0'),
+ c1('c1');
+
+ final String name;
+
+ const _E(this.name);
+}
+''');
+ }
+
+ Future<void> test_noIndex_notInt_publicClass() async {
+ await resolveTestCode('''
+class E {
+ static const E c0 = E._('c0');
+ static const E c1 = E._('c1');
+
+ final String name;
+
+ const E._(this.name);
+}
+''');
+ await assertHasAssistAt('E {', '''
+enum E {
+ c0._('c0'),
+ c1._('c1');
+
+ final String name;
+
+ const E._(this.name);
+}
+''');
+ }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/assist/test_all.dart b/pkg/analysis_server/test/src/services/correction/assist/test_all.dart
index e61b6f7..2c05865 100644
--- a/pkg/analysis_server/test/src/services/correction/assist/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/assist/test_all.dart
@@ -9,6 +9,7 @@
import 'add_return_type_test.dart' as add_return_type;
import 'add_type_annotation_test.dart' as add_type_annotation;
import 'assign_to_local_variable_test.dart' as assign_to_local_variable;
+import 'convert_class_to_enum_test.dart' as convert_class_to_enum;
import 'convert_class_to_mixin_test.dart' as convert_class_to_mixin;
import 'convert_documentation_into_block_test.dart'
as convert_documentation_into_block;
@@ -97,6 +98,7 @@
add_return_type.main();
add_type_annotation.main();
assign_to_local_variable.main();
+ convert_class_to_enum.main();
convert_class_to_mixin.main();
convert_documentation_into_block.main();
convert_documentation_into_line.main();