[Property Editor] Use dot-shorthand syntax in enum edits when possible

Bug: https://github.com/dart-lang/sdk/issues/60727
Change-Id: I41388422b0c317e7500166d29ba4f9e7b656a24f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/441862
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Elliott Brooks <elliottbrooks@google.com>
This commit is contained in:
Elliott Brooks
2025-07-24 13:40:35 -07:00
committed by Commit Queue
parent 289278b584
commit a1699ecfb0
3 changed files with 149 additions and 14 deletions
@@ -4,6 +4,7 @@
import 'package:analysis_server/src/computer/computer_documentation.dart';
import 'package:analysis_server/src/utilities/extensions/numeric.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
@@ -35,6 +36,35 @@ mixin EditableArgumentsMixin {
return dartDoc?.full;
}
/// Returns the name of an enum constant prefixed with only a dot.
///
/// If the dot-shorthands feature is not enabled, this method returns null.
String? getDotShorthandEnumConstantName(FieldElement enumConstant) {
if (!_supportsDotShorthandSyntax(enumConstant)) {
return null;
}
var name = enumConstant.name;
return name != null ? '.$name' : null;
}
/// Returns an enum constant [FieldElement] of the given [element] matching
/// the provided fully qualified name.
///
/// This method iterates through all constants of the [element] and compares
/// their fully qualified names against the [matching] string.
FieldElement? getEnumConstantMatching(
EnumElement element, {
required String matching,
}) {
for (var enumConstant in element.constants) {
if (getQualifiedEnumConstantName(enumConstant) == matching) {
return enumConstant;
}
}
return null;
}
/// Gets the argument list at [offset] that can be edited.
EditableInvocationInfo? getInvocationInfo(
ResolvedUnitResult result,
@@ -161,6 +191,11 @@ mixin EditableArgumentsMixin {
List<String> getQualifiedEnumConstantNames(EnumElement element) =>
element.constants.map(getQualifiedEnumConstantName).nonNulls.toList();
/// Determines whether or not the dot-shortands feature is enabled for the
/// given [element].
bool _supportsDotShorthandSyntax(Element element) =>
element.library?.featureSet.isEnabled(Feature.dot_shorthands) ?? false;
/// Returns the name of an enum constant prefixed with the enum name.
static String? getQualifiedEnumConstantName(FieldElement enumConstant) {
var enumName = enumConstant.enclosingElement.name;
@@ -164,6 +164,29 @@ class EditArgumentHandler extends SharedMessageHandler<EditArgumentParams, Null>
});
}
/// Computes the appropriate enum value for the String [requestValue].
///
/// This method tries to use dot-shorthand syntax for the enum value when the
/// [currentArgument] is a [DotShorthandPropertyAccess], a [SimpleIdentifier],
/// or `null`.
String _computeEnumValue({
required String? requestValue,
required FieldElement enumConstant,
required Expression? currentArgument,
}) {
var preferDotShorthand =
currentArgument is DotShorthandPropertyAccess ||
currentArgument is SimpleIdentifier ||
currentArgument == null;
var enumValue =
preferDotShorthand
? getDotShorthandEnumConstantName(enumConstant) ?? requestValue
: requestValue;
return enumValue.toString();
}
/// Computes the string of Dart code that should be used as the new value
/// for this argument.
///
@@ -188,7 +211,7 @@ class EditArgumentHandler extends SharedMessageHandler<EditArgumentParams, Null>
} else {
return error(
ServerErrorCodes.EditArgumentInvalidValue,
"The value for the parameter '${edit.name}' cannot be null",
"The value for the parameter '${edit.name}' can't be null",
);
}
}
@@ -211,14 +234,20 @@ class EditArgumentHandler extends SharedMessageHandler<EditArgumentParams, Null>
);
} else if (parameter.type case InterfaceType(
:EnumElement element,
) when value is String?) {
var allowedValues = getQualifiedEnumConstantNames(element);
if (allowedValues.contains(value)) {
return success(value.toString());
) when value is String) {
var enumConstant = getEnumConstantMatching(element, matching: value);
if (enumConstant != null) {
return success(
_computeEnumValue(
requestValue: value,
enumConstant: enumConstant,
currentArgument: argument,
),
);
} else {
return error(
ServerErrorCodes.EditArgumentInvalidValue,
"The value for the parameter '${edit.name}' should be one of ${allowedValues.map((v) => "'$v'").join(', ')} but was '$value'",
"The value for the parameter '${edit.name}' should be one of ${getQualifiedEnumConstantNames(element).map((v) => "'$v'").join(', ')} but was '$value'",
);
}
} else {
@@ -435,7 +435,7 @@ mixin SharedEditArgumentTests
originalArgs: '(x: true)',
edit: ArgumentEdit(name: 'x'),
errorCode: ServerErrorCodes.EditArgumentInvalidValue,
message: "The value for the parameter 'x' cannot be null",
message: "The value for the parameter 'x' can't be null",
);
}
@@ -483,7 +483,7 @@ mixin SharedEditArgumentTests
originalArgs: '(x: 1.0)',
edit: ArgumentEdit(name: 'x'),
errorCode: ServerErrorCodes.EditArgumentInvalidValue,
message: "The value for the parameter 'x' cannot be null",
message: "The value for the parameter 'x' can't be null",
);
}
@@ -523,6 +523,75 @@ mixin SharedEditArgumentTests
);
}
Future<void> test_type_enum_dotshorthand_addNew() async {
await _expectSimpleArgumentEdit(
additionalCode: 'enum E { one, two }',
params: '({ E? x })',
originalArgs: '()',
edit: ArgumentEdit(name: 'x', newValue: 'E.two'),
expectedArgs: '(x: .two)',
);
}
Future<void> test_type_enum_dotshorthand_disabled_addNew() async {
await _expectSimpleArgumentEdit(
additionalCode: 'enum E { one, two }',
params: '({ E? x })',
originalArgs: '()',
edit: ArgumentEdit(name: 'x', newValue: 'E.two'),
expectedArgs: '(x: E.two)',
fileComment: '// @dart = 3.8',
);
}
Future<void> test_type_enum_dotshorthand_disabled_replaceLiteral() async {
await _expectSimpleArgumentEdit(
additionalCode: 'enum E { one, two }',
params: '({ E? x })',
originalArgs: '(x: E.one)',
edit: ArgumentEdit(name: 'x', newValue: 'E.two'),
expectedArgs: '(x: E.two)',
fileComment: '// @dart = 3.8',
);
}
Future<void> test_type_enum_dotshorthand_disabled_replaceNonLiteral() async {
await _expectSimpleArgumentEdit(
additionalCode: '''
enum E { one, two }
const E myConst = E.one;
''',
params: '({ E? x })',
originalArgs: '(x: myConst)',
edit: ArgumentEdit(name: 'x', newValue: 'E.two'),
expectedArgs: '(x: E.two)',
fileComment: '// @dart = 3.8',
);
}
Future<void> test_type_enum_dotshorthand_replaceLiteral() async {
await _expectSimpleArgumentEdit(
additionalCode: 'enum E { one, two }',
params: '({ E? x })',
originalArgs: '(x: .one)',
edit: ArgumentEdit(name: 'x', newValue: 'E.two'),
expectedArgs: '(x: .two)',
);
}
Future<void> test_type_enum_dotshorthand_replaceNonLiteral() async {
await _expectSimpleArgumentEdit(
additionalCode: '''
enum E { one, two }
const E myConst = .one;
''',
params: '({ E? x })',
originalArgs: '(x: myConst)',
edit: ArgumentEdit(name: 'x', newValue: 'E.two'),
expectedArgs: '(x: .two)',
);
}
Future<void> test_type_enum_invalidType() async {
await _expectFailedEdit(
additionalCode: 'enum E { one, two }',
@@ -552,7 +621,7 @@ mixin SharedEditArgumentTests
originalArgs: '(x: E.one)',
edit: ArgumentEdit(name: 'x'),
errorCode: ServerErrorCodes.EditArgumentInvalidValue,
message: "The value for the parameter 'x' cannot be null",
message: "The value for the parameter 'x' can't be null",
);
}
@@ -575,7 +644,7 @@ const myConst = E.one;
params: '({ E? x })',
originalArgs: '(x: myConst)',
edit: ArgumentEdit(name: 'x', newValue: 'E.two'),
expectedArgs: '(x: E.two)',
expectedArgs: '(x: .two)',
);
}
@@ -604,7 +673,7 @@ const myConst = E.one;
originalArgs: '(x: 1)',
edit: ArgumentEdit(name: 'x'),
errorCode: ServerErrorCodes.EditArgumentInvalidValue,
message: "The value for the parameter 'x' cannot be null",
message: "The value for the parameter 'x' can't be null",
);
}
@@ -687,7 +756,7 @@ const myConst = E.one;
originalArgs: "(x: 'a')",
edit: ArgumentEdit(name: 'x'),
errorCode: ServerErrorCodes.EditArgumentInvalidValue,
message: "The value for the parameter 'x' cannot be null",
message: "The value for the parameter 'x' can't be null",
);
}
@@ -857,10 +926,11 @@ class MyWidget extends StatelessWidget {
required String originalArgs,
required ArgumentEdit edit,
required String expectedArgs,
String? additionalCode,
String? additionalCode = '',
String? fileComment = '',
}) async {
additionalCode ??= '';
var content = '''
$fileComment
import 'package:flutter/widgets.dart';
$additionalCode
@@ -874,6 +944,7 @@ class MyWidget extends StatelessWidget {
''';
var expectedContent = '''
>>>>>>>>>> lib/test.dart
$fileComment
import 'package:flutter/widgets.dart';
$additionalCode