8f30cb7e43
If the client supports Interactive Forms, this will allow prompting for a name for the import prefix instead of using "prefix" (or "prefix1", etc.). Includes moving some boilerplate out of each refactor into `ParameterizedRefactoringProducer` and support for a custom validation function for form fields (to validate the import prefix name in this case). Change-Id: I7150664a18944c723eeeac0a309341af2860201f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/509340 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Samuel Rawlins <srawlins@google.com>
481 lines
16 KiB
Dart
481 lines
16 KiB
Dart
// Copyright (c) 2026, 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/refactoring/add_import_prefix.dart';
|
|
import 'package:analysis_server/src/services/refactoring/move_top_level_to_file.dart';
|
|
import 'package:language_server_protocol/protocol_custom_generated.dart';
|
|
import 'package:language_server_protocol/protocol_generated.dart';
|
|
import 'package:matcher/matcher.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
|
|
import '../../src/services/refactoring/refactoring_test_support.dart';
|
|
import '../../support/interactive_forms.dart';
|
|
import '../../utils/lsp_protocol_extensions.dart';
|
|
import '../server_abstract.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(CommandResolveTest);
|
|
defineReflectiveTests(CommandResolveFileInputTest);
|
|
defineReflectiveTests(CommandResolveStringInputTest);
|
|
});
|
|
}
|
|
|
|
/// Tests file inputs in `command/resolve` using the MoveToFile refactor.
|
|
@reflectiveTest
|
|
class CommandResolveFileInputTest extends RefactoringTest
|
|
with InteractiveFormsExperimentMixin {
|
|
/// Simple file content with a single class named 'A'.
|
|
final simpleClassContent = '''
|
|
class ^A {}
|
|
''';
|
|
|
|
/// The title of the refactor when using [simpleClassContent].
|
|
final simpleClassRefactorTitle = "Move 'A' to file";
|
|
|
|
@override
|
|
String get refactoringCommandId => MoveTopLevelToFile.commandName;
|
|
|
|
@override
|
|
void setUp() {
|
|
super.setUp();
|
|
|
|
// Most of the tests here assume we support file. Tests that do not will
|
|
// explicitly unset this.
|
|
setSupportedInteractiveFormInputKinds({'file'});
|
|
|
|
// Move to file also requires file create support.
|
|
setFileCreateSupport();
|
|
}
|
|
|
|
Future<void> test_acceptsValidAnswers() async {
|
|
addTestSource(simpleClassContent);
|
|
var newFilePath = join(projectFolderPath, 'lib', 'valid_destination.dart');
|
|
var newFileUri = Uri.file(newFilePath);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(simpleClassRefactorTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Resolve again, with a valid answer.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
var field = resolvedCommand.formFields!.single;
|
|
resolvedCommand = await resolveCommand(
|
|
InteractiveExecuteCommandParams(
|
|
command: resolvedCommand.command,
|
|
arguments: resolvedCommand.arguments,
|
|
formFields: resolvedCommand.formFields,
|
|
formAnswers: [field.answer(newFileUri.toString())], // Valid answer.
|
|
),
|
|
);
|
|
|
|
// Check that the form field has gone, and that the valid answer was moved
|
|
// into the command arguments.
|
|
expect(resolvedCommand.formFields, isNull); // No more fields to complete.
|
|
var arguments = getRefactorCommandArguments(resolvedCommand.arguments);
|
|
expect(arguments, hasLength(1));
|
|
expect(arguments.single, newFileUri.toString());
|
|
}
|
|
|
|
Future<void> test_formFields_notSupported() async {
|
|
// If we don't support the 'file' kind, then we shouldn't get back any
|
|
// form fields, only the default value in the arguments.
|
|
setSupportedInteractiveFormInputKinds({'number'});
|
|
|
|
addTestSource(simpleClassContent);
|
|
var newFilePath = join(projectFolderPath, 'lib', 'a.dart');
|
|
var newFileUri = Uri.file(newFilePath);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(simpleClassRefactorTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which would normally add the form fields, but the
|
|
// only one used here is not supported by us.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Check basics.
|
|
expect(resolvedCommand.command, command.command);
|
|
expect(resolvedCommand.arguments, command.arguments);
|
|
expect(resolvedCommand.formAnswers, isNull);
|
|
expect(resolvedCommand.formFields, isNull);
|
|
|
|
// Check the arguments to the command contain the default value so the
|
|
// command will still work without the user prompt.
|
|
var refactorArguments = getRefactorCommandArguments(
|
|
resolvedCommand.arguments,
|
|
);
|
|
expect(refactorArguments, hasLength(1));
|
|
expect(refactorArguments.single, newFileUri.toString());
|
|
}
|
|
|
|
Future<void> test_formFields_supported() async {
|
|
addTestSource(simpleClassContent);
|
|
var newFilePath = join(projectFolderPath, 'lib', 'a.dart');
|
|
var newFileUri = Uri.file(newFilePath);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(simpleClassRefactorTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Check basics.
|
|
expect(resolvedCommand.command, command.command);
|
|
expect(resolvedCommand.arguments, command.arguments);
|
|
expect(resolvedCommand.formAnswers, isNull);
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
|
|
// Check the form field is what we'd expect.
|
|
var field = resolvedCommand.formFields!.single;
|
|
expect(field.type.kind, 'file');
|
|
expect(field.description, 'Move to file');
|
|
expect(field.defaultValue, newFileUri.toString());
|
|
expect(field.error, isNull);
|
|
}
|
|
|
|
Future<void> test_validates_incorrectType() async {
|
|
addTestSource(simpleClassContent);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(simpleClassRefactorTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Resolve again, with an incorrect type for the URI.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
var field = resolvedCommand.formFields!.single;
|
|
resolvedCommand = await resolveCommand(
|
|
InteractiveExecuteCommandParams(
|
|
command: resolvedCommand.command,
|
|
arguments: resolvedCommand.arguments,
|
|
formFields: resolvedCommand.formFields,
|
|
formAnswers: [field.answer(true)], // Wrong type.
|
|
),
|
|
);
|
|
|
|
// Check the field has a validation error and the current value is
|
|
// preserved.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
field = resolvedCommand.formFields!.single;
|
|
expect(field.error, contains('valid file:// URI'));
|
|
expect(resolvedCommand.formAnswers!.single, field.answer(true));
|
|
}
|
|
|
|
Future<void> test_validates_invalidUri() async {
|
|
addTestSource(simpleClassContent);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(simpleClassRefactorTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Resolve again, with an invalid value for URI.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
var field = resolvedCommand.formFields!.single;
|
|
resolvedCommand = await resolveCommand(
|
|
InteractiveExecuteCommandParams(
|
|
command: resolvedCommand.command,
|
|
arguments: resolvedCommand.arguments,
|
|
formFields: resolvedCommand.formFields,
|
|
formAnswers: [field.answer('not a valid file uri')],
|
|
),
|
|
);
|
|
|
|
// Check the field has a validation error and the current value is
|
|
// preserved.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
field = resolvedCommand.formFields!.single;
|
|
expect(field.error, contains('valid file:// URI'));
|
|
expect(
|
|
resolvedCommand.formAnswers!.single,
|
|
field.answer('not a valid file uri'),
|
|
);
|
|
}
|
|
|
|
Future<void> test_validates_notFileUri() async {
|
|
addTestSource(simpleClassContent);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(simpleClassRefactorTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Resolve again, with the wrong scheme for the URI.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
var field = resolvedCommand.formFields!.single;
|
|
resolvedCommand = await resolveCommand(
|
|
InteractiveExecuteCommandParams(
|
|
command: resolvedCommand.command,
|
|
arguments: resolvedCommand.arguments,
|
|
formFields: resolvedCommand.formFields,
|
|
formAnswers: [field.answer('https://example.org')],
|
|
),
|
|
);
|
|
|
|
// Check the field has a validation error and the current value is
|
|
// preserved.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
field = resolvedCommand.formFields!.single;
|
|
expect(field.error, contains('valid file:// URI'));
|
|
expect(
|
|
resolvedCommand.formAnswers!.single,
|
|
field.answer('https://example.org'),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Tests string inputs in `command/resolve` using the AddImportPrefix refactor.
|
|
@reflectiveTest
|
|
class CommandResolveStringInputTest extends RefactoringTest
|
|
with InteractiveFormsExperimentMixin {
|
|
final source = '''
|
|
^import 'package:test/main.dart';
|
|
''';
|
|
|
|
@override
|
|
String get refactoringCommandId => AddImportPrefix.commandName;
|
|
|
|
String get refactoringTitle => AddImportPrefix.constTitle;
|
|
|
|
@override
|
|
void setUp() {
|
|
super.setUp();
|
|
|
|
// Most of the tests here assume we support string. Tests that do not will
|
|
// explicitly unset this.
|
|
setSupportedInteractiveFormInputKinds({'string'});
|
|
}
|
|
|
|
Future<void> test_acceptsValidAnswers() async {
|
|
addTestSource(source);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(refactoringTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Resolve again, with a valid answer.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
var field = resolvedCommand.formFields!.single;
|
|
resolvedCommand = await resolveCommand(
|
|
InteractiveExecuteCommandParams(
|
|
command: resolvedCommand.command,
|
|
arguments: resolvedCommand.arguments,
|
|
formFields: resolvedCommand.formFields,
|
|
formAnswers: [field.answer('custom_prefix')],
|
|
),
|
|
);
|
|
|
|
// Check that the form field has gone, and that the valid answer was moved
|
|
// into the command arguments.
|
|
expect(resolvedCommand.formFields, isNull);
|
|
var arguments = getRefactorCommandArguments(resolvedCommand.arguments);
|
|
expect(arguments, hasLength(1));
|
|
expect(arguments.single, 'custom_prefix');
|
|
}
|
|
|
|
Future<void> test_formFields_notSupported() async {
|
|
// If we don't support the 'string' kind, then we shouldn't get back any
|
|
// form fields, only the default value in the arguments.
|
|
setSupportedInteractiveFormInputKinds({'file'});
|
|
|
|
addTestSource(source);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(refactoringTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which would normally add the form fields, but the
|
|
// only one used here is not supported by us.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Check basics.
|
|
expect(resolvedCommand.command, command.command);
|
|
expect(resolvedCommand.formAnswers, isNull);
|
|
expect(resolvedCommand.formFields, isNull);
|
|
|
|
// Check the arguments to the command contain the default value so the
|
|
// command will still work without the user prompt.
|
|
var refactorArguments = getRefactorCommandArguments(
|
|
resolvedCommand.arguments,
|
|
);
|
|
expect(refactorArguments, hasLength(1));
|
|
expect(refactorArguments.single, 'main');
|
|
}
|
|
|
|
Future<void> test_formFields_supported() async {
|
|
addTestSource(source);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(refactoringTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Check basics.
|
|
expect(resolvedCommand.command, command.command);
|
|
expect(resolvedCommand.formAnswers, isNull);
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
|
|
// Because this argument is optional (because we only expect it when using
|
|
// Interactive Forms but the refactor can be used without), args are
|
|
// populated with the default during resolve.
|
|
var arguments = getRefactorCommandArguments(resolvedCommand.arguments);
|
|
expect(arguments, hasLength(1));
|
|
expect(arguments.single, 'main');
|
|
|
|
// Check the form field is what we'd expect.
|
|
var field = resolvedCommand.formFields!.single;
|
|
expect(field.type.kind, 'string');
|
|
expect(field.description, 'Import Prefix');
|
|
expect(field.defaultValue, 'main');
|
|
expect(field.error, isNull);
|
|
}
|
|
|
|
Future<void> test_validates_invalidValue() async {
|
|
addTestSource(source);
|
|
|
|
await initializeServer();
|
|
var action = await expectCodeActionWithTitle(refactoringTitle);
|
|
var command = action.asCommand;
|
|
|
|
// Resolve the command, which should add the outstanding form fields.
|
|
var resolvedCommand = await resolveCommand(
|
|
ExecuteCommandParams(
|
|
command: command.command,
|
|
arguments: command.arguments,
|
|
),
|
|
);
|
|
|
|
// Resolve again, with an invalid prefix name.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
var field = resolvedCommand.formFields!.single;
|
|
resolvedCommand = await resolveCommand(
|
|
InteractiveExecuteCommandParams(
|
|
command: resolvedCommand.command,
|
|
arguments: resolvedCommand.arguments,
|
|
formFields: resolvedCommand.formFields,
|
|
formAnswers: [field.answer('Invalid Prefix')],
|
|
),
|
|
);
|
|
|
|
// Check the field has a validation error and the current value is
|
|
// preserved.
|
|
expect(resolvedCommand.formFields, hasLength(1));
|
|
field = resolvedCommand.formFields!.single;
|
|
expect(field.error, "Import prefix name must not contain ' '.");
|
|
expect(resolvedCommand.formAnswers!.single, field.answer('Invalid Prefix'));
|
|
|
|
// Resolve again with a valid value and ensure the field is completed.
|
|
resolvedCommand = await resolveCommand(
|
|
InteractiveExecuteCommandParams(
|
|
command: resolvedCommand.command,
|
|
arguments: resolvedCommand.arguments,
|
|
formFields: resolvedCommand.formFields,
|
|
formAnswers: [field.answer('valid_prefix')],
|
|
),
|
|
);
|
|
|
|
expect(resolvedCommand.formFields, isNull);
|
|
var arguments = getRefactorCommandArguments(resolvedCommand.arguments);
|
|
expect(arguments, hasLength(1));
|
|
expect(arguments.single, 'valid_prefix');
|
|
}
|
|
}
|
|
|
|
@reflectiveTest
|
|
class CommandResolveTest extends AbstractLspAnalysisServerTest {
|
|
Future<void> test_returnsInputForUnknownCommand() async {
|
|
// Basic command that only includes the normal fields from
|
|
// ExecuteCommandParams. This ensures calling resolve() for commands that
|
|
// don't need form input will return the same result (with no formFields).
|
|
var command = InteractiveExecuteCommandParams(
|
|
command: 'my_unknown_command',
|
|
arguments: [1, 'two'],
|
|
);
|
|
|
|
await initialize();
|
|
|
|
var resolvedCommand = await resolveCommand(command);
|
|
|
|
expect(resolvedCommand, command);
|
|
}
|
|
}
|
|
|
|
/// A temporary mixin that sets the flag to enable the Interactive Forms
|
|
/// experiment setting.
|
|
mixin InteractiveFormsExperimentMixin on RefactoringTest {
|
|
@override
|
|
Future<void> initializeServer({
|
|
bool experimentalOptInFlag = true,
|
|
// We default this to true for these tests, though it's false in the
|
|
// the super implementation.
|
|
bool experimentalInteractiveForms = true,
|
|
}) {
|
|
return super.initializeServer(
|
|
experimentalOptInFlag: experimentalOptInFlag,
|
|
experimentalInteractiveForms: experimentalInteractiveForms,
|
|
);
|
|
}
|
|
}
|