From a1699ecfb0721578e6991af22701a272639e5082 Mon Sep 17 00:00:00 2001 From: Elliott Brooks Date: Thu, 24 Jul 2025 13:40:35 -0700 Subject: [PATCH] [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 Commit-Queue: Elliott Brooks --- .../editable_arguments_mixin.dart | 35 ++++++++ .../handler_edit_argument.dart | 41 +++++++-- .../shared/shared_edit_argument_tests.dart | 87 +++++++++++++++++-- 3 files changed, 149 insertions(+), 14 deletions(-) diff --git a/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/editable_arguments_mixin.dart b/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/editable_arguments_mixin.dart index 8b761891b33..7da98806da8 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/editable_arguments_mixin.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/editable_arguments_mixin.dart @@ -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 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; diff --git a/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/handler_edit_argument.dart b/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/handler_edit_argument.dart index e0a3c78b2af..aa8c6fc5bd6 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/handler_edit_argument.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/custom/editable_arguments/handler_edit_argument.dart @@ -164,6 +164,29 @@ class EditArgumentHandler extends SharedMessageHandler }); } + /// 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 } 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 ); } 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 { diff --git a/pkg/analysis_server/test/shared/shared_edit_argument_tests.dart b/pkg/analysis_server/test/shared/shared_edit_argument_tests.dart index a6645b63cd1..529d94d87d6 100644 --- a/pkg/analysis_server/test/shared/shared_edit_argument_tests.dart +++ b/pkg/analysis_server/test/shared/shared_edit_argument_tests.dart @@ -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 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 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 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 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 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 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 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