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();