From 93621ccfc1a81b3d9848436c5b736553808aa28e Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Wed, 15 Jan 2025 12:30:26 -0800 Subject: [PATCH] [analysis_server] Move all EditableArgument tests to a shared mixin and run for legacy This moves all tests (without any changes) to a shared mixin, and then applies that mixin to the original test class. It also adds a new test class for LSP-over-Legacy, meaning the tests will now run for both protocols. There are two failing tests when using LSP-over-Legacy so they are overridden with `@FailingTest()` temporarily. Change-Id: I8c7efcf8f5d85d874b3483f4f3953d4d8b68a31a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/404621 Reviewed-by: Brian Wilkerson Commit-Queue: Phil Quitslund Reviewed-by: Samuel Rawlins Reviewed-by: Phil Quitslund --- .../test/lsp/editable_arguments_test.dart | 1262 +--------------- .../editable_arguments_test.dart | 49 + .../test/lsp_over_legacy/test_all.dart | 2 + .../shared_editable_arguments_tests.dart | 1270 +++++++++++++++++ 4 files changed, 1324 insertions(+), 1259 deletions(-) create mode 100644 pkg/analysis_server/test/lsp_over_legacy/editable_arguments_test.dart create mode 100644 pkg/analysis_server/test/shared/shared_editable_arguments_tests.dart diff --git a/pkg/analysis_server/test/lsp/editable_arguments_test.dart b/pkg/analysis_server/test/lsp/editable_arguments_test.dart index 844f0a8c539e..5aa0c093103d 100644 --- a/pkg/analysis_server/test/lsp/editable_arguments_test.dart +++ b/pkg/analysis_server/test/lsp/editable_arguments_test.dart @@ -2,12 +2,9 @@ // 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/lsp_protocol/protocol.dart'; -import 'package:analyzer/src/test_utilities/test_code_format.dart'; -import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../utils/test_code_extensions.dart'; +import '../shared/shared_editable_arguments_tests.dart'; import 'server_abstract.dart'; void main() { @@ -17,1265 +14,12 @@ void main() { } @reflectiveTest -class EditableArgumentsTest extends AbstractLspAnalysisServerTest { - late TestCode code; - - /// Initializes the server with [content] and fetches editable arguments. - Future getEditableArgumentsFor( - String content, { - bool open = true, - }) async { - code = TestCode.parse(''' -import 'package:flutter/widgets.dart'; - -$content -'''); - newFile(mainFilePath, code.code); - await initialize(); - if (open) { - await openFile(mainFileUri, code.code); - } - await initialAnalysis; - return await getEditableArguments(mainFileUri, code.position.position); - } - - Matcher hasArg(Matcher matcher) { - return hasArgs(contains(matcher)); - } - - Matcher hasArgNamed(String argumentName) { - return hasArg(isArg(argumentName)); - } - - Matcher hasArgs(Matcher matcher) { - return isA().having( - (arguments) => arguments.arguments, - 'arguments', - matcher, - ); - } - - Matcher isArg( - String name, { - Object? type = anything, - Object? value = anything, - Object? displayValue = anything, - Object? hasArgument = anything, - Object? isDefault = anything, - Object? isRequired = anything, - Object? isNullable = anything, - Object? isEditable = anything, - Object? notEditableReason = anything, - Object? options = anything, - }) { - return isA() - .having((arg) => arg.name, 'name', name) - .having((arg) => arg.type, 'type', type) - .having((arg) => arg.value, 'value', value) - .having((arg) => arg.displayValue, 'displayValue', displayValue) - .having((arg) => arg.hasArgument, 'hasArgument', hasArgument) - .having((arg) => arg.isDefault, 'isDefault', isDefault) - .having((arg) => arg.isRequired, 'isRequired', isRequired) - .having((arg) => arg.isNullable, 'isNullable', isNullable) - .having((arg) => arg.isEditable, 'isEditable', isEditable) - .having( - (arg) => arg.notEditableReason, - 'notEditableReason', - notEditableReason, - ) - .having((arg) => arg.options, 'options', options) - // Some extra checks that should be true for all. - .having( - (arg) => - arg.value == null || - arg.value?.toString() != arg.displayValue?.toString(), - 'different value and displayValues', - isTrue, - ) - .having( - (arg) => (arg.notEditableReason == null) == arg.isEditable, - 'notEditableReason must be supplied if isEditable=false', - isTrue, - ) - .having( - (arg) => arg.value == null || arg.isEditable, - 'isEditable must be true if there is a value', - isTrue, - ) - .having( - (arg) => - arg.type == 'enum' - ? (arg.options?.isNotEmpty ?? false) - : arg.options == null, - 'enum types must have options / non-enums must not have options', - isTrue, - ); - } - +class EditableArgumentsTest extends SharedAbstractLspAnalysisServerTest + with SharedEditableArgumentsTests { @override void setUp() { super.setUp(); writeTestPackageConfig(flutter: true); } - - test_hasArgument() async { - failTestOnErrorDiagnostic = false; - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget( - int? aPositionalSupplied, - int? aPositionalNotSupplied, { - int? aNamedSupplied, - int? aNamedNotSupplied, - }); - - @override - Widget build(BuildContext context) => MyW^idget(1, aNamedSupplied: 1); -} -'''); - expect( - result, - hasArgs( - unorderedEquals([ - isArg('aPositionalSupplied', hasArgument: true), - isArg('aPositionalNotSupplied', hasArgument: false), - isArg('aNamedSupplied', hasArgument: true), - isArg('aNamedNotSupplied', hasArgument: false), - ]), - ), - ); - } - - test_isEditable_false_positional_optional() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget([int? a, int? b, int? c]); - - @override - Widget build(BuildContext context) => MyW^idget(1); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('a', isEditable: true), - isArg('b', isEditable: true), - isArg( - 'c', - // c is not editable because it is not guaranteed that we can insert - // a default value for b (it could be a private value or require - // imports). - isEditable: false, - notEditableReason: - "A value for the 3rd parameter can't be added until a value " - 'for all preceding positional parameters have been added.', - ), - ]), - ), - ); - } - - test_isEditable_false_positional_required1() async { - failTestOnErrorDiagnostic = false; - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(int a, int b); - - @override - Widget build(BuildContext context) => MyW^idget(); -} -'''); - expect( - result, - hasArg( - // b is not editable because there are missing required previous - // arguments (a). - isArg( - 'b', - isEditable: false, - notEditableReason: - "A value for the 2nd parameter can't be added until a value " - 'for all preceding positional parameters have been added.', - ), - ), - ); - } - - test_isEditable_false_positional_required2() async { - failTestOnErrorDiagnostic = false; - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(int a, int b, int c); - - @override - Widget build(BuildContext context) => MyW^idget(1); -} -'''); - expect( - result, - hasArg( - // c is not editable because there are missing required previous - // arguments (b). - isArg( - 'c', - isEditable: false, - notEditableReason: - "A value for the 3rd parameter can't be added until a value " - 'for all preceding positional parameters have been added.', - ), - ), - ); - } - - test_isEditable_false_string_adjacent() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(String s); - - @override - Widget build(BuildContext context) => MyW^idget('a' 'b'); -} -'''); - expect( - result, - hasArg( - isArg( - 's', - type: 'string', - value: isNull, - displayValue: 'ab', - isDefault: false, - isEditable: false, - notEditableReason: "Adjacent strings can't be edited", - ), - ), - ); - } - - test_isEditable_false_string_interpolated() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(String s); - - @override - Widget build(BuildContext context) => MyW^idget('${context.runtimeType}'); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg( - 's', - type: 'string', - value: isNull, - displayValue: r"'${context.runtimeType}'", - isEditable: false, - notEditableReason: "Interpolated strings can't be edited", - ), - ]), - ), - ); - } - - test_isEditable_false_string_withNewlines() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(String sEscaped, String sLiteral); - - @override - Widget build(BuildContext context) => MyW^idget( - 'a\nb', - """ -a -b -""", - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg( - 'sEscaped', - type: 'string', - value: isNull, - displayValue: 'a\nb', - isEditable: false, - notEditableReason: "Strings containing newlines can't be edited", - ), - isArg( - 'sLiteral', - type: 'string', - value: isNull, - displayValue: 'a\nb\n', - isEditable: false, - notEditableReason: "Strings containing newlines can't be edited", - ), - ]), - ), - ); - } - - test_isEditable_true_named() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget({int? a, int? b, int? c}); - - @override - Widget build(BuildContext context) => MyW^idget(a: 1); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('a', isEditable: true), - isArg('b', isEditable: true), - isArg('c', isEditable: true), - ]), - ), - ); - } - - test_isEditable_true_positional_required() async { - failTestOnErrorDiagnostic = false; - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(int a, int b); - - @override - Widget build(BuildContext context) => MyW^idget(1); -} -'''); - expect( - result, - hasArg( - isArg( - 'b', - // b is editable because it's the next argument and we don't need - // to add anything additional. - isEditable: true, - ), - ), - ); - } - - test_isEditable_true_string_dollar_escaped() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(String s); - - @override - Widget build(BuildContext context) => MyW^idget('\${1}'); -} -'''); - expect( - result, - hasArg( - isArg( - 's', - type: 'string', - value: r'${1}', - displayValue: isNull, - isEditable: true, - ), - ), - ); - } - - test_isEditable_true_string_dollar_raw() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(String s); - - @override - Widget build(BuildContext context) => MyW^idget(r'${1}'); -} -'''); - expect( - result, - hasArg( - isArg( - 's', - type: 'string', - value: r'${1}', - displayValue: isNull, - isEditable: true, - ), - ), - ); - } - - test_isEditable_true_string_tripleQuoted_withoutNewlines() async { - var result = await getEditableArgumentsFor(r''' -class MyWidget extends StatelessWidget { - const MyWidget(String s); - - @override - Widget build(BuildContext context) => MyW^idget("""string_value"""); -} -'''); - expect( - result, - hasArg( - isArg( - 's', - type: 'string', - value: 'string_value', - displayValue: isNull, - isEditable: true, - ), - ), - ); - } - - test_isNullable() async { - failTestOnErrorDiagnostic = false; - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget( - int aPositional, - int? aPositionalNullable, { - int? aNamed, - required int aRequiredNamed, - required int? aRequiredNamedNullable - }); - - @override - Widget build(BuildContext context) => MyW^idget(); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('aPositional', isNullable: false), - isArg('aPositionalNullable', isNullable: true), - isArg('aNamed', isNullable: true), - isArg('aRequiredNamed', isNullable: false), - isArg('aRequiredNamedNullable', isNullable: true), - ]), - ), - ); - } - - test_isRequired() async { - failTestOnErrorDiagnostic = false; - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget( - int aPositional, - int? aPositionalNullable, { - int? aNamed, - required int aRequiredNamed, - required int? aRequiredNamedNullable - }); - - @override - Widget build(BuildContext context) => MyW^idget(); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('aPositional', isRequired: true), - isArg('aPositionalNullable', isRequired: true), - isArg('aNamed', isRequired: false), - isArg('aRequiredNamed', isRequired: true), - isArg('aRequiredNamedNullable', isRequired: true), - ]), - ), - ); - } - - test_location_bad_extensionMethod_noWidgetFactory() async { - var result = await getEditableArgumentsFor(''' -extension on MyWidget { - Widget padded(String a1) => this; -} - -class MyWidget extends StatelessWidget { - const MyWidget(); - const MyWidget.foo(String a1); - - @override - Widget build(BuildContext context) => this.pad^ded('value1'); -} -'''); - expect(result, isNull); - } - - test_location_bad_functionInvocation() async { - var result = await getEditableArgumentsFor(''' -MyWidget create(String a1) => throw ''; - -class MyWidget extends StatelessWidget { - @override - Widget build(BuildContext context) => crea^te('value1'); -} -'''); - expect(result, isNull); - } - - test_location_bad_methodInvocation() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - @widgetFactory - MyWidget create(String a1) => throw ''; - - @override - Widget build(BuildContext context) => crea^te('value1'); -} -'''); - expect(result, isNull); - } - - test_location_bad_unnamedConstructor_notWidget() async { - var result = await getEditableArgumentsFor(''' -class MyWidget { - const MyWidget(String a1); - - @override - MyWidget build(BuildContext context) => MyW^idget('value1'); -} -'''); - expect(result, isNull); - } - - test_location_good_argumentList_argumentName() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({required String a1 }); - - @override - Widget build(BuildContext context) => MyWidget(a^1: 'value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_argumentList_literalValue() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({required String a1 }); - - @override - Widget build(BuildContext context) => MyWidget(a1: 'val^ue1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_argumentList_nestedInvocation() async { - var result = await getEditableArgumentsFor(''' -String getString() => ''; - -class MyWidget extends StatelessWidget { - const MyWidget(String a1); - - @override - Widget build(BuildContext context) => MyWidget(getS^tring()); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_argumentList_nestedInvocation_arguments() async { - var result = await getEditableArgumentsFor(''' -String getString(String s) => s; - -class MyWidget extends StatelessWidget { - const MyWidget(String a1); - - @override - Widget build(BuildContext context) => MyWidget(getString('valu^e1')); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_argumentList_parens_afterOpen() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({required String a1 }); - - @override - Widget build(BuildContext context) => MyWidget(^a1: 'value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_argumentList_parens_beforeClose() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({required String a1 }); - - @override - Widget build(BuildContext context) => MyWidget(a1: 'value1'^); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_argumentList_parens_beforeOpen() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({required String a1 }); - - @override - Widget build(BuildContext context) => MyWidget^(a1: 'value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_extensionMethod_constructorTarget() async { - var result = await getEditableArgumentsFor(''' -extension on MyWidget { - @widgetFactory - Widget padded(String a1) => this; -} - -class MyWidget extends StatelessWidget { - const MyWidget(); - const MyWidget.foo(String a1); - - @override - Widget build(BuildContext context) => MyWidget().pad^ded('value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_extensionMethod_thisTarget() async { - var result = await getEditableArgumentsFor(''' -extension on MyWidget { - @widgetFactory - Widget padded(String a1) => this; -} - -class MyWidget extends StatelessWidget { - const MyWidget.foo(String a1); - - @override - Widget build(BuildContext context) => this.pad^ded('value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_extensionMethod_variableTarget() async { - var result = await getEditableArgumentsFor(''' -extension on MyWidget { - @widgetFactory - Widget padded(String a1) => this; -} - -class MyWidget extends StatelessWidget { - const MyWidget(); - const MyWidget.foo(String a1); - - @override - Widget build(BuildContext context) { - MyWidget? foo; - return foo!.pad^ded('value1'); - } -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_namedConstructor_className() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget.foo(String a1); - - @override - Widget build(BuildContext context) => MyW^idget.foo('value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_namedConstructor_constructorName() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget.foo(String a1); - - @override - Widget build(BuildContext context) => MyWidget.f^oo('value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - test_location_good_unnamedConstructor() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget(String a1); - - @override - Widget build(BuildContext context) => MyW^idget('value1'); -} -'''); - expect(result, hasArgNamed('a1')); - } - - /// Arguments should be returned in the order of the parameters in the source - /// code. This keeps things consistent across different instances of the same - /// Widget class and prevents the order from changing as a user adds/removes - /// arguments. - /// - /// If an editor wants to sort provided arguments first (and keep these stable - /// across add/removes) it could still do so client-side, whereas if server - /// orders them that way, the opposite (using source-order) is not possible. - test_order() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({ - int c1 = 0, - int c2 = 0, - int a1 = 0, - int a2 = 0, - int b1 = 0, - int b2 = 0, - }); - - @override - Widget build(BuildContext context) => MyW^idget(b1: 1, a1: 1, c1: 1); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('c1'), - isArg('c2'), - isArg('a1'), - isArg('a2'), - isArg('b1'), - isArg('b2'), - ]), - ), - ); - } - - test_textDocument_closedFile() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget(String a1); - - @override - Widget build(BuildContext context) => MyW^idget('value1'); -} -'''); - - // Verify initial content of 1. - expect( - result!.textDocument, - isA() - .having((td) => td.uri, 'uri', mainFileUri) - .having((td) => td.version, 'version', 1), - ); - - // Close the file. - await closeFile(mainFileUri); - - // Verify new results have null version. - result = await getEditableArguments(mainFileUri, code.position.position); - expect( - result!.textDocument, - isA() - .having((td) => td.uri, 'uri', mainFileUri) - .having((td) => td.version, 'version', isNull), - ); - } - - test_textDocument_unopenedFile() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget(String a1); - - @override - Widget build(BuildContext context) => MyW^idget('value1'); -} -''', open: false); - - // Verify null version for unopened file. - expect( - result!.textDocument, - isA() - .having((td) => td.uri, 'uri', mainFileUri) - .having((td) => td.version, 'version', null), - ); - } - - test_textDocument_versions() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget(String a1); - - @override - Widget build(BuildContext context) => MyW^idget('value1'); -} -'''); - - // Verify initial content of 1. - expect( - result!.textDocument, - isA() - .having((td) => td.uri, 'uri', mainFileUri) - .having((td) => td.version, 'version', 1), - ); - - // Update the content to v5. - await replaceFile(5, mainFileUri, '${code.code}\n// extra comment'); - - // Verify new results have version 5. - result = await getEditableArguments(mainFileUri, code.position.position); - expect( - result!.textDocument, - isA() - .having((td) => td.uri, 'uri', mainFileUri) - .having((td) => td.version, 'version', 5), - ); - } - - test_type_bool() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({ - bool supplied = true, - bool suppliedAsDefault = true, - bool notSupplied = true, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - supplied: false, - suppliedAsDefault: true, - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('supplied', type: 'bool', value: false, isDefault: false), - isArg( - 'suppliedAsDefault', - type: 'bool', - value: true, - isDefault: true, - ), - isArg('notSupplied', type: 'bool', value: true, isDefault: true), - ]), - ), - ); - } - - test_type_bool_nonLiterals() async { - var result = await getEditableArgumentsFor(''' -var myVar = true; -const myConst = true; -class MyWidget extends StatelessWidget { - const MyWidget({ - bool? aVar, - bool? aConst, - bool? aExpr, - bool? aConstExpr, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - aVar: myVar, - aConst: myConst, - aExpr: DateTime.now().isBefore(DateTime.now()), - aConstExpr: 1 == 2, - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('aVar', type: 'bool', value: isNull, displayValue: 'myVar'), - isArg('aConst', type: 'bool', value: true, displayValue: 'myConst'), - isArg( - 'aExpr', - type: 'bool', - value: isNull, - displayValue: 'DateTime.now().isBefore(DateTime.now())', - ), - isArg( - 'aConstExpr', - type: 'bool', - value: false, - displayValue: '1 == 2', - ), - ]), - ), - ); - } - - test_type_double() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({ - double supplied = 1.0, - double suppliedAsDefault = 1.0, - double notSupplied = 1.0, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - supplied: 2.0, - suppliedAsDefault: 1.0, - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('supplied', type: 'double', value: 2.0, isDefault: false), - isArg( - 'suppliedAsDefault', - type: 'double', - value: 1.0, - isDefault: true, - ), - isArg('notSupplied', type: 'double', value: 1.0, isDefault: true), - ]), - ), - ); - } - - test_type_double_intLiterals() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({ - double supplied = 1, - double suppliedAsDefault = 1, - double notSupplied = 1, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - supplied: 2, - suppliedAsDefault: 1, - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('supplied', type: 'double', value: 2, isDefault: false), - isArg('suppliedAsDefault', type: 'double', value: 1, isDefault: true), - isArg('notSupplied', type: 'double', value: 1, isDefault: true), - ]), - ), - ); - } - - test_type_double_nonLiterals() async { - var result = await getEditableArgumentsFor(''' -var myVar = 1.0; -const myConst = 1.0; -class MyWidget extends StatelessWidget { - const MyWidget({ - double? aVar, - double? aConst, - double? aExpr, - double? aConstExpr, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - aVar: myVar, - aConst: myConst, - aExpr: DateTime.now().millisecondsSinceEpoch.toDouble(), - aConstExpr: 1.0 + myConst, - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('aVar', type: 'double', value: isNull, displayValue: 'myVar'), - isArg('aConst', type: 'double', value: 1.0, displayValue: 'myConst'), - isArg( - 'aExpr', - type: 'double', - value: isNull, - displayValue: 'DateTime.now().millisecondsSinceEpoch.toDouble()', - ), - isArg( - 'aConstExpr', - type: 'double', - value: 2.0, - displayValue: '1.0 + myConst', - ), - ]), - ), - ); - } - - test_type_enum() async { - var result = await getEditableArgumentsFor(''' -enum E { one, two } -class MyWidget extends StatelessWidget { - const MyWidget({ - E supplied = E.one, - E suppliedAsDefault = E.one, - E notSupplied = E.one, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - supplied: E.two, - suppliedAsDefault: E.one, - ); -} -'''); - - var optionsMatcher = equals(['E.one', 'E.two']); - expect( - result, - hasArgs( - orderedEquals([ - isArg( - 'supplied', - type: 'enum', - value: 'E.two', - isDefault: false, - options: optionsMatcher, - ), - isArg( - 'suppliedAsDefault', - type: 'enum', - value: 'E.one', - isDefault: true, - options: optionsMatcher, - ), - isArg( - 'notSupplied', - type: 'enum', - value: 'E.one', - isDefault: true, - options: optionsMatcher, - ), - ]), - ), - ); - } - - test_type_enum_nonLiterals() async { - var result = await getEditableArgumentsFor(''' -enum E { one, two } -var myVar = E.one; -const myConst = E.one; -class MyWidget extends StatelessWidget { - const MyWidget({ - E? aVar, - E? aConst, - E? aExpr, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - aVar: myVar, - aConst: myConst, - aExpr: E.values.first, - ); -} -'''); - - var optionsMatcher = equals(['E.one', 'E.two']); - expect( - result, - hasArgs( - orderedEquals([ - isArg( - 'aVar', - type: 'enum', - value: isNull, - displayValue: 'myVar', - options: optionsMatcher, - ), - isArg( - 'aConst', - type: 'enum', - value: 'E.one', - displayValue: 'myConst', - options: optionsMatcher, - ), - isArg( - 'aExpr', - type: 'enum', - value: isNull, - displayValue: 'E.values.first', - options: optionsMatcher, - ), - ]), - ), - ); - } - - test_type_int() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({ - int supplied = 1, - int suppliedAsDefault = 1, - int notSupplied = 1, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - supplied: 2, - suppliedAsDefault: 1, - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('supplied', type: 'int', value: 2, isDefault: false), - isArg('suppliedAsDefault', type: 'int', value: 1, isDefault: true), - isArg('notSupplied', type: 'int', value: 1, isDefault: true), - ]), - ), - ); - } - - test_type_int_nonLiterals() async { - var result = await getEditableArgumentsFor(''' -var myVar = 1; -const myConst = 1; -class MyWidget extends StatelessWidget { - const MyWidget({ - int? aVar, - int? aConst, - int? aExpr, - int? aConstExpr, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - aVar: myVar, - aConst: myConst, - aExpr: DateTime.now().millisecondsSinceEpoch, - aConstExpr: 1 + myConst, - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('aVar', type: 'int', value: isNull, displayValue: 'myVar'), - isArg('aConst', type: 'int', value: 1, displayValue: 'myConst'), - isArg( - 'aExpr', - type: 'int', - value: isNull, - displayValue: 'DateTime.now().millisecondsSinceEpoch', - ), - isArg( - 'aConstExpr', - type: 'int', - value: 2, - displayValue: '1 + myConst', - ), - ]), - ), - ); - } - - test_type_string() async { - var result = await getEditableArgumentsFor(''' -class MyWidget extends StatelessWidget { - const MyWidget({ - String supplied = 'a', - String suppliedAsDefault = 'a', - String notSupplied = 'a', - }); - - @override - Widget build(BuildContext context) => MyW^idget( - supplied: 'b', - suppliedAsDefault: 'a', - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('supplied', type: 'string', value: 'b', isDefault: false), - isArg( - 'suppliedAsDefault', - type: 'string', - value: 'a', - isDefault: true, - ), - isArg('notSupplied', type: 'string', value: 'a', isDefault: true), - ]), - ), - ); - } - - test_type_string_nonLiterals() async { - var result = await getEditableArgumentsFor(''' -var myVar = 'a'; -const myConst = 'a'; -class MyWidget extends StatelessWidget { - const MyWidget({ - String? aVar, - String? aConst, - String? aExpr, - String? aConstExpr, - }); - - @override - Widget build(BuildContext context) => MyW^idget( - aVar: myVar, - aConst: myConst, - aExpr: DateTime.now().toString(), - aConstExpr: 'a' + 'b', - ); -} -'''); - expect( - result, - hasArgs( - orderedEquals([ - isArg('aVar', type: 'string', value: isNull, displayValue: 'myVar'), - isArg('aConst', type: 'string', value: 'a', displayValue: 'myConst'), - isArg( - 'aExpr', - type: 'string', - value: isNull, - displayValue: 'DateTime.now().toString()', - ), - isArg( - 'aConstExpr', - type: 'string', - value: 'ab', - displayValue: "'a' + 'b'", - ), - ]), - ), - ); - } } diff --git a/pkg/analysis_server/test/lsp_over_legacy/editable_arguments_test.dart b/pkg/analysis_server/test/lsp_over_legacy/editable_arguments_test.dart new file mode 100644 index 000000000000..5e56fffbce26 --- /dev/null +++ b/pkg/analysis_server/test/lsp_over_legacy/editable_arguments_test.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2025, 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:test_reflective_loader/test_reflective_loader.dart'; + +import '../shared/shared_editable_arguments_tests.dart'; +import 'abstract_lsp_over_legacy.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(EditableArgumentsTest); + }); +} + +@reflectiveTest +class EditableArgumentsTest extends SharedLspOverLegacyTest + with + // Tests are defined in SharedEditableArgumentsTests because they + // are shared and run for both LSP and Legacy servers. + SharedEditableArgumentsTests { + @override + Future initializeServer() async { + await waitForTasksFinished(); + } + + @override + Future setUp() async { + await super.setUp(); + + writeTestPackageConfig(flutter: true); + } + + @override + @FailingTest(reason: 'Document versions not currently supported for legacy') + test_textDocument_closedFile() { + // TODO(dantup): Implement support for version numbers in the legacy + // protocol. + return super.test_textDocument_closedFile(); + } + + @override + @FailingTest(reason: 'Document versions not currently supported for legacy') + test_textDocument_versions() { + // TODO(dantup): Implement support for version numbers in the legacy + // protocol. + return super.test_textDocument_versions(); + } +} diff --git a/pkg/analysis_server/test/lsp_over_legacy/test_all.dart b/pkg/analysis_server/test/lsp_over_legacy/test_all.dart index bd04d654b221..afc8547e3e10 100644 --- a/pkg/analysis_server/test/lsp_over_legacy/test_all.dart +++ b/pkg/analysis_server/test/lsp_over_legacy/test_all.dart @@ -10,6 +10,7 @@ import 'dart_text_document_content_provider_test.dart' import 'document_color_test.dart' as document_color; import 'document_highlights_test.dart' as document_highlights; import 'document_symbols_test.dart' as document_symbols; +import 'editable_arguments_test.dart' as editable_arguments; import 'format_test.dart' as format; import 'hover_test.dart' as hover; import 'implementation_test.dart' as implementation; @@ -27,6 +28,7 @@ void main() { document_color.main; document_highlights.main(); document_symbols.main(); + editable_arguments.main(); format.main(); hover.main(); implementation.main(); diff --git a/pkg/analysis_server/test/shared/shared_editable_arguments_tests.dart b/pkg/analysis_server/test/shared/shared_editable_arguments_tests.dart new file mode 100644 index 000000000000..dfa990ae933a --- /dev/null +++ b/pkg/analysis_server/test/shared/shared_editable_arguments_tests.dart @@ -0,0 +1,1270 @@ +// Copyright (c) 2025, 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/lsp_protocol/protocol.dart'; +import 'package:analyzer/src/test_utilities/test_code_format.dart'; +import 'package:test/test.dart'; + +import '../lsp/request_helpers_mixin.dart'; +import '../utils/test_code_extensions.dart'; +import 'shared_test_interface.dart'; + +/// Shared editable arguments tests that are used by both LSP + Legacy server +/// tests. +mixin SharedEditableArgumentsTests + on SharedTestInterface, LspRequestHelpersMixin { + late TestCode code; + + /// Initializes the server with [content] and fetches editable arguments. + Future getEditableArgumentsFor( + String content, { + bool open = true, + }) async { + code = TestCode.parse(''' +import 'package:flutter/widgets.dart'; + +$content +'''); + createFile(testFilePath, code.code); + await initializeServer(); + if (open) { + await openFile(testFileUri, code.code); + } + await currentAnalysis; + return await getEditableArguments(testFileUri, code.position.position); + } + + Matcher hasArg(Matcher matcher) { + return hasArgs(contains(matcher)); + } + + Matcher hasArgNamed(String argumentName) { + return hasArg(isArg(argumentName)); + } + + Matcher hasArgs(Matcher matcher) { + return isA().having( + (arguments) => arguments.arguments, + 'arguments', + matcher, + ); + } + + Matcher isArg( + String name, { + Object? type = anything, + Object? value = anything, + Object? displayValue = anything, + Object? hasArgument = anything, + Object? isDefault = anything, + Object? isRequired = anything, + Object? isNullable = anything, + Object? isEditable = anything, + Object? notEditableReason = anything, + Object? options = anything, + }) { + return isA() + .having((arg) => arg.name, 'name', name) + .having((arg) => arg.type, 'type', type) + .having((arg) => arg.value, 'value', value) + .having((arg) => arg.displayValue, 'displayValue', displayValue) + .having((arg) => arg.hasArgument, 'hasArgument', hasArgument) + .having((arg) => arg.isDefault, 'isDefault', isDefault) + .having((arg) => arg.isRequired, 'isRequired', isRequired) + .having((arg) => arg.isNullable, 'isNullable', isNullable) + .having((arg) => arg.isEditable, 'isEditable', isEditable) + .having( + (arg) => arg.notEditableReason, + 'notEditableReason', + notEditableReason, + ) + .having((arg) => arg.options, 'options', options) + // Some extra checks that should be true for all. + .having( + (arg) => + arg.value == null || + arg.value?.toString() != arg.displayValue?.toString(), + 'different value and displayValues', + isTrue, + ) + .having( + (arg) => (arg.notEditableReason == null) == arg.isEditable, + 'notEditableReason must be supplied if isEditable=false', + isTrue, + ) + .having( + (arg) => arg.value == null || arg.isEditable, + 'isEditable must be true if there is a value', + isTrue, + ) + .having( + (arg) => + arg.type == 'enum' + ? (arg.options?.isNotEmpty ?? false) + : arg.options == null, + 'enum types must have options / non-enums must not have options', + isTrue, + ); + } + + test_hasArgument() async { + failTestOnErrorDiagnostic = false; + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget( + int? aPositionalSupplied, + int? aPositionalNotSupplied, { + int? aNamedSupplied, + int? aNamedNotSupplied, + }); + + @override + Widget build(BuildContext context) => MyW^idget(1, aNamedSupplied: 1); +} +'''); + expect( + result, + hasArgs( + unorderedEquals([ + isArg('aPositionalSupplied', hasArgument: true), + isArg('aPositionalNotSupplied', hasArgument: false), + isArg('aNamedSupplied', hasArgument: true), + isArg('aNamedNotSupplied', hasArgument: false), + ]), + ), + ); + } + + test_isEditable_false_positional_optional() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget([int? a, int? b, int? c]); + + @override + Widget build(BuildContext context) => MyW^idget(1); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('a', isEditable: true), + isArg('b', isEditable: true), + isArg( + 'c', + // c is not editable because it is not guaranteed that we can insert + // a default value for b (it could be a private value or require + // imports). + isEditable: false, + notEditableReason: + "A value for the 3rd parameter can't be added until a value " + 'for all preceding positional parameters have been added.', + ), + ]), + ), + ); + } + + test_isEditable_false_positional_required1() async { + failTestOnErrorDiagnostic = false; + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(int a, int b); + + @override + Widget build(BuildContext context) => MyW^idget(); +} +'''); + expect( + result, + hasArg( + // b is not editable because there are missing required previous + // arguments (a). + isArg( + 'b', + isEditable: false, + notEditableReason: + "A value for the 2nd parameter can't be added until a value " + 'for all preceding positional parameters have been added.', + ), + ), + ); + } + + test_isEditable_false_positional_required2() async { + failTestOnErrorDiagnostic = false; + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(int a, int b, int c); + + @override + Widget build(BuildContext context) => MyW^idget(1); +} +'''); + expect( + result, + hasArg( + // c is not editable because there are missing required previous + // arguments (b). + isArg( + 'c', + isEditable: false, + notEditableReason: + "A value for the 3rd parameter can't be added until a value " + 'for all preceding positional parameters have been added.', + ), + ), + ); + } + + test_isEditable_false_string_adjacent() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(String s); + + @override + Widget build(BuildContext context) => MyW^idget('a' 'b'); +} +'''); + expect( + result, + hasArg( + isArg( + 's', + type: 'string', + value: isNull, + displayValue: 'ab', + isDefault: false, + isEditable: false, + notEditableReason: "Adjacent strings can't be edited", + ), + ), + ); + } + + test_isEditable_false_string_interpolated() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(String s); + + @override + Widget build(BuildContext context) => MyW^idget('${context.runtimeType}'); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg( + 's', + type: 'string', + value: isNull, + displayValue: r"'${context.runtimeType}'", + isEditable: false, + notEditableReason: "Interpolated strings can't be edited", + ), + ]), + ), + ); + } + + test_isEditable_false_string_withNewlines() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(String sEscaped, String sLiteral); + + @override + Widget build(BuildContext context) => MyW^idget( + 'a\nb', + """ +a +b +""", + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg( + 'sEscaped', + type: 'string', + value: isNull, + displayValue: 'a\nb', + isEditable: false, + notEditableReason: "Strings containing newlines can't be edited", + ), + isArg( + 'sLiteral', + type: 'string', + value: isNull, + displayValue: 'a\nb\n', + isEditable: false, + notEditableReason: "Strings containing newlines can't be edited", + ), + ]), + ), + ); + } + + test_isEditable_true_named() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget({int? a, int? b, int? c}); + + @override + Widget build(BuildContext context) => MyW^idget(a: 1); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('a', isEditable: true), + isArg('b', isEditable: true), + isArg('c', isEditable: true), + ]), + ), + ); + } + + test_isEditable_true_positional_required() async { + failTestOnErrorDiagnostic = false; + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(int a, int b); + + @override + Widget build(BuildContext context) => MyW^idget(1); +} +'''); + expect( + result, + hasArg( + isArg( + 'b', + // b is editable because it's the next argument and we don't need + // to add anything additional. + isEditable: true, + ), + ), + ); + } + + test_isEditable_true_string_dollar_escaped() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(String s); + + @override + Widget build(BuildContext context) => MyW^idget('\${1}'); +} +'''); + expect( + result, + hasArg( + isArg( + 's', + type: 'string', + value: r'${1}', + displayValue: isNull, + isEditable: true, + ), + ), + ); + } + + test_isEditable_true_string_dollar_raw() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(String s); + + @override + Widget build(BuildContext context) => MyW^idget(r'${1}'); +} +'''); + expect( + result, + hasArg( + isArg( + 's', + type: 'string', + value: r'${1}', + displayValue: isNull, + isEditable: true, + ), + ), + ); + } + + test_isEditable_true_string_tripleQuoted_withoutNewlines() async { + var result = await getEditableArgumentsFor(r''' +class MyWidget extends StatelessWidget { + const MyWidget(String s); + + @override + Widget build(BuildContext context) => MyW^idget("""string_value"""); +} +'''); + expect( + result, + hasArg( + isArg( + 's', + type: 'string', + value: 'string_value', + displayValue: isNull, + isEditable: true, + ), + ), + ); + } + + test_isNullable() async { + failTestOnErrorDiagnostic = false; + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget( + int aPositional, + int? aPositionalNullable, { + int? aNamed, + required int aRequiredNamed, + required int? aRequiredNamedNullable + }); + + @override + Widget build(BuildContext context) => MyW^idget(); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('aPositional', isNullable: false), + isArg('aPositionalNullable', isNullable: true), + isArg('aNamed', isNullable: true), + isArg('aRequiredNamed', isNullable: false), + isArg('aRequiredNamedNullable', isNullable: true), + ]), + ), + ); + } + + test_isRequired() async { + failTestOnErrorDiagnostic = false; + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget( + int aPositional, + int? aPositionalNullable, { + int? aNamed, + required int aRequiredNamed, + required int? aRequiredNamedNullable + }); + + @override + Widget build(BuildContext context) => MyW^idget(); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('aPositional', isRequired: true), + isArg('aPositionalNullable', isRequired: true), + isArg('aNamed', isRequired: false), + isArg('aRequiredNamed', isRequired: true), + isArg('aRequiredNamedNullable', isRequired: true), + ]), + ), + ); + } + + test_location_bad_extensionMethod_noWidgetFactory() async { + var result = await getEditableArgumentsFor(''' +extension on MyWidget { + Widget padded(String a1) => this; +} + +class MyWidget extends StatelessWidget { + const MyWidget(); + const MyWidget.foo(String a1); + + @override + Widget build(BuildContext context) => this.pad^ded('value1'); +} +'''); + expect(result, isNull); + } + + test_location_bad_functionInvocation() async { + var result = await getEditableArgumentsFor(''' +MyWidget create(String a1) => throw ''; + +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) => crea^te('value1'); +} +'''); + expect(result, isNull); + } + + test_location_bad_methodInvocation() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + @widgetFactory + MyWidget create(String a1) => throw ''; + + @override + Widget build(BuildContext context) => crea^te('value1'); +} +'''); + expect(result, isNull); + } + + test_location_bad_unnamedConstructor_notWidget() async { + var result = await getEditableArgumentsFor(''' +class MyWidget { + const MyWidget(String a1); + + @override + MyWidget build(BuildContext context) => MyW^idget('value1'); +} +'''); + expect(result, isNull); + } + + test_location_good_argumentList_argumentName() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({required String a1 }); + + @override + Widget build(BuildContext context) => MyWidget(a^1: 'value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_argumentList_literalValue() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({required String a1 }); + + @override + Widget build(BuildContext context) => MyWidget(a1: 'val^ue1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_argumentList_nestedInvocation() async { + var result = await getEditableArgumentsFor(''' +String getString() => ''; + +class MyWidget extends StatelessWidget { + const MyWidget(String a1); + + @override + Widget build(BuildContext context) => MyWidget(getS^tring()); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_argumentList_nestedInvocation_arguments() async { + var result = await getEditableArgumentsFor(''' +String getString(String s) => s; + +class MyWidget extends StatelessWidget { + const MyWidget(String a1); + + @override + Widget build(BuildContext context) => MyWidget(getString('valu^e1')); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_argumentList_parens_afterOpen() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({required String a1 }); + + @override + Widget build(BuildContext context) => MyWidget(^a1: 'value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_argumentList_parens_beforeClose() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({required String a1 }); + + @override + Widget build(BuildContext context) => MyWidget(a1: 'value1'^); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_argumentList_parens_beforeOpen() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({required String a1 }); + + @override + Widget build(BuildContext context) => MyWidget^(a1: 'value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_extensionMethod_constructorTarget() async { + var result = await getEditableArgumentsFor(''' +extension on MyWidget { + @widgetFactory + Widget padded(String a1) => this; +} + +class MyWidget extends StatelessWidget { + const MyWidget(); + const MyWidget.foo(String a1); + + @override + Widget build(BuildContext context) => MyWidget().pad^ded('value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_extensionMethod_thisTarget() async { + var result = await getEditableArgumentsFor(''' +extension on MyWidget { + @widgetFactory + Widget padded(String a1) => this; +} + +class MyWidget extends StatelessWidget { + const MyWidget.foo(String a1); + + @override + Widget build(BuildContext context) => this.pad^ded('value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_extensionMethod_variableTarget() async { + var result = await getEditableArgumentsFor(''' +extension on MyWidget { + @widgetFactory + Widget padded(String a1) => this; +} + +class MyWidget extends StatelessWidget { + const MyWidget(); + const MyWidget.foo(String a1); + + @override + Widget build(BuildContext context) { + MyWidget? foo; + return foo!.pad^ded('value1'); + } +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_namedConstructor_className() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget.foo(String a1); + + @override + Widget build(BuildContext context) => MyW^idget.foo('value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_namedConstructor_constructorName() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget.foo(String a1); + + @override + Widget build(BuildContext context) => MyWidget.f^oo('value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + test_location_good_unnamedConstructor() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget(String a1); + + @override + Widget build(BuildContext context) => MyW^idget('value1'); +} +'''); + expect(result, hasArgNamed('a1')); + } + + /// Arguments should be returned in the order of the parameters in the source + /// code. This keeps things consistent across different instances of the same + /// Widget class and prevents the order from changing as a user adds/removes + /// arguments. + /// + /// If an editor wants to sort provided arguments first (and keep these stable + /// across add/removes) it could still do so client-side, whereas if server + /// orders them that way, the opposite (using source-order) is not possible. + test_order() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({ + int c1 = 0, + int c2 = 0, + int a1 = 0, + int a2 = 0, + int b1 = 0, + int b2 = 0, + }); + + @override + Widget build(BuildContext context) => MyW^idget(b1: 1, a1: 1, c1: 1); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('c1'), + isArg('c2'), + isArg('a1'), + isArg('a2'), + isArg('b1'), + isArg('b2'), + ]), + ), + ); + } + + test_textDocument_closedFile() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget(String a1); + + @override + Widget build(BuildContext context) => MyW^idget('value1'); +} +'''); + + // Verify initial content of 1. + expect( + result!.textDocument, + isA() + .having((td) => td.uri, 'uri', testFileUri) + .having((td) => td.version, 'version', 1), + ); + + // Close the file. + await closeFile(testFileUri); + + // Verify new results have null version. + result = await getEditableArguments(testFileUri, code.position.position); + expect( + result!.textDocument, + isA() + .having((td) => td.uri, 'uri', testFileUri) + .having((td) => td.version, 'version', isNull), + ); + } + + test_textDocument_unopenedFile() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget(String a1); + + @override + Widget build(BuildContext context) => MyW^idget('value1'); +} +''', open: false); + + // Verify null version for unopened file. + expect( + result!.textDocument, + isA() + .having((td) => td.uri, 'uri', testFileUri) + .having((td) => td.version, 'version', null), + ); + } + + test_textDocument_versions() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget(String a1); + + @override + Widget build(BuildContext context) => MyW^idget('value1'); +} +'''); + + // Verify initial content of 1. + expect( + result!.textDocument, + isA() + .having((td) => td.uri, 'uri', testFileUri) + .having((td) => td.version, 'version', 1), + ); + + // Update the content to v5. + await replaceFile(5, testFileUri, '${code.code}\n// extra comment'); + + // Verify new results have version 5. + result = await getEditableArguments(testFileUri, code.position.position); + expect( + result!.textDocument, + isA() + .having((td) => td.uri, 'uri', testFileUri) + .having((td) => td.version, 'version', 5), + ); + } + + test_type_bool() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({ + bool supplied = true, + bool suppliedAsDefault = true, + bool notSupplied = true, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + supplied: false, + suppliedAsDefault: true, + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('supplied', type: 'bool', value: false, isDefault: false), + isArg( + 'suppliedAsDefault', + type: 'bool', + value: true, + isDefault: true, + ), + isArg('notSupplied', type: 'bool', value: true, isDefault: true), + ]), + ), + ); + } + + test_type_bool_nonLiterals() async { + var result = await getEditableArgumentsFor(''' +var myVar = true; +const myConst = true; +class MyWidget extends StatelessWidget { + const MyWidget({ + bool? aVar, + bool? aConst, + bool? aExpr, + bool? aConstExpr, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + aVar: myVar, + aConst: myConst, + aExpr: DateTime.now().isBefore(DateTime.now()), + aConstExpr: 1 == 2, + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('aVar', type: 'bool', value: isNull, displayValue: 'myVar'), + isArg('aConst', type: 'bool', value: true, displayValue: 'myConst'), + isArg( + 'aExpr', + type: 'bool', + value: isNull, + displayValue: 'DateTime.now().isBefore(DateTime.now())', + ), + isArg( + 'aConstExpr', + type: 'bool', + value: false, + displayValue: '1 == 2', + ), + ]), + ), + ); + } + + test_type_double() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({ + double supplied = 1.0, + double suppliedAsDefault = 1.0, + double notSupplied = 1.0, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + supplied: 2.0, + suppliedAsDefault: 1.0, + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('supplied', type: 'double', value: 2.0, isDefault: false), + isArg( + 'suppliedAsDefault', + type: 'double', + value: 1.0, + isDefault: true, + ), + isArg('notSupplied', type: 'double', value: 1.0, isDefault: true), + ]), + ), + ); + } + + test_type_double_intLiterals() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({ + double supplied = 1, + double suppliedAsDefault = 1, + double notSupplied = 1, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + supplied: 2, + suppliedAsDefault: 1, + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('supplied', type: 'double', value: 2, isDefault: false), + isArg('suppliedAsDefault', type: 'double', value: 1, isDefault: true), + isArg('notSupplied', type: 'double', value: 1, isDefault: true), + ]), + ), + ); + } + + test_type_double_nonLiterals() async { + var result = await getEditableArgumentsFor(''' +var myVar = 1.0; +const myConst = 1.0; +class MyWidget extends StatelessWidget { + const MyWidget({ + double? aVar, + double? aConst, + double? aExpr, + double? aConstExpr, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + aVar: myVar, + aConst: myConst, + aExpr: DateTime.now().millisecondsSinceEpoch.toDouble(), + aConstExpr: 1.0 + myConst, + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('aVar', type: 'double', value: isNull, displayValue: 'myVar'), + isArg('aConst', type: 'double', value: 1.0, displayValue: 'myConst'), + isArg( + 'aExpr', + type: 'double', + value: isNull, + displayValue: 'DateTime.now().millisecondsSinceEpoch.toDouble()', + ), + isArg( + 'aConstExpr', + type: 'double', + value: 2.0, + displayValue: '1.0 + myConst', + ), + ]), + ), + ); + } + + test_type_enum() async { + var result = await getEditableArgumentsFor(''' +enum E { one, two } +class MyWidget extends StatelessWidget { + const MyWidget({ + E supplied = E.one, + E suppliedAsDefault = E.one, + E notSupplied = E.one, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + supplied: E.two, + suppliedAsDefault: E.one, + ); +} +'''); + + var optionsMatcher = equals(['E.one', 'E.two']); + expect( + result, + hasArgs( + orderedEquals([ + isArg( + 'supplied', + type: 'enum', + value: 'E.two', + isDefault: false, + options: optionsMatcher, + ), + isArg( + 'suppliedAsDefault', + type: 'enum', + value: 'E.one', + isDefault: true, + options: optionsMatcher, + ), + isArg( + 'notSupplied', + type: 'enum', + value: 'E.one', + isDefault: true, + options: optionsMatcher, + ), + ]), + ), + ); + } + + test_type_enum_nonLiterals() async { + var result = await getEditableArgumentsFor(''' +enum E { one, two } +var myVar = E.one; +const myConst = E.one; +class MyWidget extends StatelessWidget { + const MyWidget({ + E? aVar, + E? aConst, + E? aExpr, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + aVar: myVar, + aConst: myConst, + aExpr: E.values.first, + ); +} +'''); + + var optionsMatcher = equals(['E.one', 'E.two']); + expect( + result, + hasArgs( + orderedEquals([ + isArg( + 'aVar', + type: 'enum', + value: isNull, + displayValue: 'myVar', + options: optionsMatcher, + ), + isArg( + 'aConst', + type: 'enum', + value: 'E.one', + displayValue: 'myConst', + options: optionsMatcher, + ), + isArg( + 'aExpr', + type: 'enum', + value: isNull, + displayValue: 'E.values.first', + options: optionsMatcher, + ), + ]), + ), + ); + } + + test_type_int() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({ + int supplied = 1, + int suppliedAsDefault = 1, + int notSupplied = 1, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + supplied: 2, + suppliedAsDefault: 1, + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('supplied', type: 'int', value: 2, isDefault: false), + isArg('suppliedAsDefault', type: 'int', value: 1, isDefault: true), + isArg('notSupplied', type: 'int', value: 1, isDefault: true), + ]), + ), + ); + } + + test_type_int_nonLiterals() async { + var result = await getEditableArgumentsFor(''' +var myVar = 1; +const myConst = 1; +class MyWidget extends StatelessWidget { + const MyWidget({ + int? aVar, + int? aConst, + int? aExpr, + int? aConstExpr, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + aVar: myVar, + aConst: myConst, + aExpr: DateTime.now().millisecondsSinceEpoch, + aConstExpr: 1 + myConst, + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('aVar', type: 'int', value: isNull, displayValue: 'myVar'), + isArg('aConst', type: 'int', value: 1, displayValue: 'myConst'), + isArg( + 'aExpr', + type: 'int', + value: isNull, + displayValue: 'DateTime.now().millisecondsSinceEpoch', + ), + isArg( + 'aConstExpr', + type: 'int', + value: 2, + displayValue: '1 + myConst', + ), + ]), + ), + ); + } + + test_type_string() async { + var result = await getEditableArgumentsFor(''' +class MyWidget extends StatelessWidget { + const MyWidget({ + String supplied = 'a', + String suppliedAsDefault = 'a', + String notSupplied = 'a', + }); + + @override + Widget build(BuildContext context) => MyW^idget( + supplied: 'b', + suppliedAsDefault: 'a', + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('supplied', type: 'string', value: 'b', isDefault: false), + isArg( + 'suppliedAsDefault', + type: 'string', + value: 'a', + isDefault: true, + ), + isArg('notSupplied', type: 'string', value: 'a', isDefault: true), + ]), + ), + ); + } + + test_type_string_nonLiterals() async { + var result = await getEditableArgumentsFor(''' +var myVar = 'a'; +const myConst = 'a'; +class MyWidget extends StatelessWidget { + const MyWidget({ + String? aVar, + String? aConst, + String? aExpr, + String? aConstExpr, + }); + + @override + Widget build(BuildContext context) => MyW^idget( + aVar: myVar, + aConst: myConst, + aExpr: DateTime.now().toString(), + aConstExpr: 'a' + 'b', + ); +} +'''); + expect( + result, + hasArgs( + orderedEquals([ + isArg('aVar', type: 'string', value: isNull, displayValue: 'myVar'), + isArg('aConst', type: 'string', value: 'a', displayValue: 'myConst'), + isArg( + 'aExpr', + type: 'string', + value: isNull, + displayValue: 'DateTime.now().toString()', + ), + isArg( + 'aConstExpr', + type: 'string', + value: 'ab', + displayValue: "'a' + 'b'", + ), + ]), + ), + ); + } +}