[migrate][analysis_server] LSP Protocol for migrate tool.
This some basic scaffolding for the migrate tool. We'll add a protocol to the analysis server with the following parameters and result: Parameters - `uris`: Workspaces/packages to be migrated Result - `summary`: Information about fixes that could not be applied automatically. (e.g. if there was a conflict) or information about what fixes were applied and what SDK version the pubspec has been changed to. - `edit`: A list of edits to be applied. There are no interesting tests yet, but I hope to have a suite of tests for the next change. Fixes: https://github.com/dart-lang/sdk/issues/63247 Change-Id: I77508720acb17af5ec86675fd3f3045e2a610bf2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/496801 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Kallen Tu <kallentu@google.com>
This commit is contained in:
committed by
dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
72600b32af
commit
81a14586a9
@@ -163,6 +163,7 @@ abstract final class CustomMethods {
|
||||
);
|
||||
static const summary = Method('dart/textDocument/summary');
|
||||
static const super_ = Method('dart/textDocument/super');
|
||||
static const migrate = Method('dart/workspace/migrate');
|
||||
static const imports = Method('dart/textDocument/imports');
|
||||
|
||||
/// Method for requesting the set of editable arguments at a location in a
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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/lsp_protocol/protocol.dart';
|
||||
import 'package:analysis_server/src/lsp/constants.dart';
|
||||
import 'package:analysis_server/src/lsp/error_or.dart';
|
||||
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
|
||||
import 'package:analyzer/file_system/file_system.dart';
|
||||
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class MigrateHandler
|
||||
extends SharedMessageHandler<DartMigrateParams, DartMigrateResult> {
|
||||
MigrateHandler(super.server);
|
||||
|
||||
@override
|
||||
Method get handlesMessage => CustomMethods.migrate;
|
||||
|
||||
@override
|
||||
LspJsonHandler<DartMigrateParams> get jsonHandler =>
|
||||
DartMigrateParams.jsonHandler;
|
||||
|
||||
@override
|
||||
bool get requiresTrustedCaller => true;
|
||||
|
||||
@override
|
||||
Future<ErrorOr<DartMigrateResult>> handle(
|
||||
DartMigrateParams params,
|
||||
MessageInfo message,
|
||||
CancellationToken token,
|
||||
) async {
|
||||
var validationResult = _validateMigrationTargets(params.uris);
|
||||
if (validationResult.isError) {
|
||||
return failure(validationResult);
|
||||
}
|
||||
|
||||
// TODO(kallentu): Add a registry of pre-migration lints to enable based
|
||||
// on pubspec version.
|
||||
|
||||
// TODO(kallentu): Support bumping the pubspec SDK constraint and
|
||||
// reanalyzing with the new version constraint.
|
||||
|
||||
// TODO(kallentu): Add a registry of post-migration lints to enable based
|
||||
// on pubspec version.
|
||||
|
||||
// TODO(kallentu): Fix post-migration lints.
|
||||
return success(DartMigrateResult(summary: 'Not implemented yet.'));
|
||||
}
|
||||
|
||||
/// Validates that all provided [uris] are directories and each directory
|
||||
/// contains a `pubspec.yaml` file.
|
||||
///
|
||||
/// Returns an error if any URI points to a file, does not exist, or does
|
||||
/// not contain a `pubspec.yaml` file.
|
||||
ErrorOr<void> _validateMigrationTargets(List<DocumentUri> uris) {
|
||||
for (var uri in uris) {
|
||||
var pathResult = pathOfUri(uri);
|
||||
if (pathResult.isError) {
|
||||
return failure(pathResult);
|
||||
}
|
||||
|
||||
var path = pathResult.resultOrNull!;
|
||||
var resource = server.resourceProvider.getResource(path);
|
||||
if (!resource.exists) {
|
||||
return error(
|
||||
ErrorCodes.InvalidParams,
|
||||
"The path '$path' doesn't exist.",
|
||||
);
|
||||
}
|
||||
if (resource is! Folder) {
|
||||
return error(
|
||||
ErrorCodes.InvalidParams,
|
||||
"The path '$path' doesn't refer to a package or pub workspace"
|
||||
' directory.',
|
||||
);
|
||||
}
|
||||
|
||||
var pubspecFile = resource.getChildAssumingFile(file_paths.pubspecYaml);
|
||||
if (!pubspecFile.exists) {
|
||||
return error(
|
||||
ErrorCodes.InvalidParams,
|
||||
"The directory '$path' doesn't contain a 'pubspec.yaml' file.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
var pubspecContent = pubspecFile.readAsStringSync();
|
||||
var pubspec = loadYamlNode(
|
||||
pubspecContent,
|
||||
sourceUrl: pubspecFile.toUri(),
|
||||
);
|
||||
if (pubspec is YamlMap && pubspec['resolution'] == 'workspace') {
|
||||
return error(
|
||||
ErrorCodes.InvalidParams,
|
||||
"The directory '$path' is part of a workspace and can't be migrated"
|
||||
' independently.',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return error(ErrorCodes.InvalidParams, "Failed to parse '$path': $e");
|
||||
}
|
||||
}
|
||||
return success(null);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import 'package:analysis_server/src/lsp/handlers/custom/handler_diagnostic_serve
|
||||
import 'package:analysis_server/src/lsp/handlers/custom/handler_experimental_echo.dart';
|
||||
import 'package:analysis_server/src/lsp/handlers/custom/handler_get_widget_previews.dart';
|
||||
import 'package:analysis_server/src/lsp/handlers/custom/handler_imports.dart';
|
||||
import 'package:analysis_server/src/lsp/handlers/custom/handler_migrate.dart';
|
||||
import 'package:analysis_server/src/lsp/handlers/custom/handler_reanalyze.dart';
|
||||
import 'package:analysis_server/src/lsp/handlers/custom/handler_summary.dart';
|
||||
import 'package:analysis_server/src/lsp/handlers/custom/handler_super.dart';
|
||||
@@ -143,6 +144,7 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
|
||||
ImplementationHandler.new,
|
||||
IncomingCallHierarchyHandler.new,
|
||||
InlineValueHandler.new,
|
||||
MigrateHandler.new,
|
||||
OutgoingCallHierarchyHandler.new,
|
||||
PrepareCallHierarchyHandler.new,
|
||||
PrepareTypeHierarchyHandler.new,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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/lsp_protocol/protocol.dart';
|
||||
import 'package:analysis_server/src/lsp/constants.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import '../tool/lsp_spec/matchers.dart';
|
||||
import 'server_abstract.dart';
|
||||
|
||||
void main() {
|
||||
defineReflectiveSuite(() {
|
||||
defineReflectiveTests(MigrateTest);
|
||||
});
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class MigrateTest extends AbstractLspAnalysisServerTest {
|
||||
Future<void> test_error_directoryWithoutPubspec() async {
|
||||
await initialize();
|
||||
|
||||
var request = makeRequest(
|
||||
CustomMethods.migrate,
|
||||
DartMigrateParams(uris: [projectFolderUri]),
|
||||
);
|
||||
var response = await sendRequestToServer(request);
|
||||
|
||||
expect(
|
||||
response.error,
|
||||
isResponseError(
|
||||
ErrorCodes.InvalidParams,
|
||||
message:
|
||||
"The directory '/home/my_project' doesn't contain a 'pubspec.yaml' "
|
||||
'file.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_error_fileUri() async {
|
||||
await initialize();
|
||||
|
||||
newFile(mainFilePath, '');
|
||||
|
||||
var request = makeRequest(
|
||||
CustomMethods.migrate,
|
||||
DartMigrateParams(uris: [mainFileUri]),
|
||||
);
|
||||
var response = await sendRequestToServer(request);
|
||||
|
||||
expect(
|
||||
response.error,
|
||||
isResponseError(
|
||||
ErrorCodes.InvalidParams,
|
||||
message:
|
||||
"The path '/home/my_project/lib/main.dart' doesn't refer to a "
|
||||
'package or pub workspace directory.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_error_fileUri_multipleWithOneInvalid() async {
|
||||
await initialize();
|
||||
|
||||
newFile(pubspecFilePath, 'name: test_project');
|
||||
|
||||
var validUri = projectFolderUri;
|
||||
var invalidUri = Uri.parse('http://example.com');
|
||||
|
||||
var request = makeRequest(
|
||||
CustomMethods.migrate,
|
||||
DartMigrateParams(uris: [validUri, invalidUri]),
|
||||
);
|
||||
var response = await sendRequestToServer(request);
|
||||
|
||||
expect(
|
||||
response.error,
|
||||
isResponseError(
|
||||
ServerErrorCodes.invalidFilePath,
|
||||
message: contains("URI scheme 'http' is not supported"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_error_nonExistentDirectory() async {
|
||||
await initialize();
|
||||
|
||||
var dirUri = Uri.file(convertPath('/non/existent/dir'));
|
||||
var request = makeRequest(
|
||||
CustomMethods.migrate,
|
||||
DartMigrateParams(uris: [dirUri]),
|
||||
);
|
||||
var response = await sendRequestToServer(request);
|
||||
|
||||
expect(
|
||||
response.error,
|
||||
isResponseError(
|
||||
ErrorCodes.InvalidParams,
|
||||
message: "The path '/non/existent/dir' doesn't exist.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_error_workspacePackage() async {
|
||||
await initialize();
|
||||
|
||||
newFile(pubspecFilePath, '''
|
||||
name: test_project
|
||||
resolution: workspace
|
||||
''');
|
||||
|
||||
var request = makeRequest(
|
||||
CustomMethods.migrate,
|
||||
DartMigrateParams(uris: [projectFolderUri]),
|
||||
);
|
||||
var response = await sendRequestToServer(request);
|
||||
|
||||
expect(
|
||||
response.error,
|
||||
isResponseError(
|
||||
ErrorCodes.InvalidParams,
|
||||
message:
|
||||
"The directory '/home/my_project' is part of a workspace and can't "
|
||||
'be migrated independently.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> test_validDirectory() async {
|
||||
await initialize();
|
||||
|
||||
newFile(pubspecFilePath, 'name: test_project');
|
||||
|
||||
var request = makeRequest(
|
||||
CustomMethods.migrate,
|
||||
DartMigrateParams(uris: [projectFolderUri]),
|
||||
);
|
||||
var response = await sendRequestToServer(request);
|
||||
|
||||
expect(response.error, isNull);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import 'initialization_test.dart' as initialization;
|
||||
import 'inlay_hint_test.dart' as inlay_hint;
|
||||
import 'inline_value_test.dart' as inline_value;
|
||||
import 'mapping_test.dart' as mapping;
|
||||
import 'migrate_test.dart' as migrate;
|
||||
import 'open_uri_test.dart' as open_uri;
|
||||
import 'outline_test.dart' as outline;
|
||||
import 'priority_files_test.dart' as priority_files;
|
||||
@@ -113,6 +114,7 @@ void main() {
|
||||
inline_value.main();
|
||||
lsp_packet_transformer.main();
|
||||
mapping.main();
|
||||
migrate.main();
|
||||
open_uri.main();
|
||||
outline.main();
|
||||
priority_files.main();
|
||||
|
||||
@@ -295,6 +295,20 @@ Returns: `FlutterWidgetPreviews | null`
|
||||
|
||||
Returns the set of detected Flutter Widget Previews in the analyzed project.
|
||||
|
||||
### dart/workspace/migrate Method
|
||||
|
||||
Direction: Client -> Server
|
||||
Params: `DartMigrateParams`
|
||||
Returns: `DartMigrateResult`
|
||||
|
||||
Migrates the provided pub workspace folders or non-pub workspace packages to the
|
||||
latest Dart version. For packages that are part of a pub workspace, only the
|
||||
workspace root should be passed. Migrating individual packages within a
|
||||
workspace independently is not supported.
|
||||
|
||||
The response includes a summary of the results and a `WorkspaceEdit` containing
|
||||
the changes to be applied.
|
||||
|
||||
### dart/openUri Notification
|
||||
|
||||
Direction: Server -> Client
|
||||
|
||||
@@ -722,6 +722,35 @@ List<LspEntity> getCustomClasses() {
|
||||
interface('DocumentSummary', [
|
||||
field('summary', type: 'String', canBeNull: true),
|
||||
]),
|
||||
|
||||
// Types for `dart/workspace/migrate`.
|
||||
interface('DartMigrateParams', [
|
||||
field(
|
||||
'uris',
|
||||
type: 'DocumentUri',
|
||||
array: true,
|
||||
comment:
|
||||
'The URIs of the directories (packages or workspaces) to migrate. '
|
||||
'Individual file URIs are not supported.',
|
||||
),
|
||||
]),
|
||||
interface('DartMigrateResult', [
|
||||
field(
|
||||
'summary',
|
||||
type: 'String',
|
||||
canBeNull: true,
|
||||
comment:
|
||||
'A summary of the migration results, detailing which fixes '
|
||||
'succeeded, which fixes failed to be applied, and the new '
|
||||
'SDK version constraint applied to the pubspec.yaml.',
|
||||
),
|
||||
field(
|
||||
'edit',
|
||||
type: 'WorkspaceEdit',
|
||||
canBeNull: true,
|
||||
comment: 'The edits to be applied to the workspace.',
|
||||
),
|
||||
]),
|
||||
];
|
||||
return customTypes;
|
||||
}
|
||||
|
||||
@@ -736,6 +736,31 @@ bool _canParseUri(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _canParseWorkspaceEdit(
|
||||
Map<String, Object?> map, LspJsonReporter reporter, String fieldName,
|
||||
{required bool allowsUndefined, required bool allowsNull}) {
|
||||
reporter.push(fieldName);
|
||||
try {
|
||||
if (!allowsUndefined && !map.containsKey(fieldName)) {
|
||||
reporter.reportError('must not be undefined');
|
||||
return false;
|
||||
}
|
||||
final value = map[fieldName];
|
||||
final nullCheck = allowsNull || allowsUndefined;
|
||||
if (!nullCheck && value == null) {
|
||||
reporter.reportError('must not be null');
|
||||
return false;
|
||||
}
|
||||
if ((!nullCheck || value != null) &&
|
||||
!WorkspaceEdit.canParse(value, reporter)) {
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
reporter.pop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Either2<int, String> _eitherIntString(Object? value) {
|
||||
return value is int
|
||||
? Either2.t1(value)
|
||||
@@ -1253,6 +1278,132 @@ class DartDiagnosticServer implements ToJsonable {
|
||||
}
|
||||
}
|
||||
|
||||
class DartMigrateParams implements ToJsonable {
|
||||
static const jsonHandler = LspJsonHandler(
|
||||
DartMigrateParams.canParse,
|
||||
DartMigrateParams.fromJson,
|
||||
);
|
||||
|
||||
/// The URIs of the directories (packages or workspaces) to migrate.
|
||||
/// Individual file URIs are not supported.
|
||||
final List<DocumentUri> uris;
|
||||
|
||||
DartMigrateParams({
|
||||
required this.uris,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => lspHashCode(uris);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DartMigrateParams &&
|
||||
other.runtimeType == DartMigrateParams &&
|
||||
const DeepCollectionEquality().equals(uris, other.uris);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() {
|
||||
var result = <String, Object?>{};
|
||||
result['uris'] = uris.map((uri) => uri.toString()).toList();
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => jsonEncoder.convert(toJson());
|
||||
|
||||
static bool canParse(Object? obj, LspJsonReporter reporter) {
|
||||
if (obj is Map<String, Object?>) {
|
||||
return _canParseListUri(obj, reporter, 'uris',
|
||||
allowsUndefined: false, allowsNull: false);
|
||||
} else {
|
||||
reporter.reportError('must be of type DartMigrateParams');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static DartMigrateParams fromJson(Map<String, Object?> json) {
|
||||
final urisJson = json['uris'];
|
||||
final uris = (urisJson as List<Object?>)
|
||||
.map((item) => Uri.parse(item as String))
|
||||
.toList();
|
||||
return DartMigrateParams(
|
||||
uris: uris,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DartMigrateResult implements ToJsonable {
|
||||
static const jsonHandler = LspJsonHandler(
|
||||
DartMigrateResult.canParse,
|
||||
DartMigrateResult.fromJson,
|
||||
);
|
||||
|
||||
/// The edits to be applied to the workspace.
|
||||
final WorkspaceEdit? edit;
|
||||
|
||||
/// A summary of the migration results, detailing which fixes succeeded, which
|
||||
/// fixes failed to be applied, and the new SDK version constraint applied to
|
||||
/// the pubspec.yaml.
|
||||
final String? summary;
|
||||
|
||||
DartMigrateResult({
|
||||
this.edit,
|
||||
this.summary,
|
||||
});
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
edit,
|
||||
summary,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DartMigrateResult &&
|
||||
other.runtimeType == DartMigrateResult &&
|
||||
edit == other.edit &&
|
||||
summary == other.summary;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() {
|
||||
var result = <String, Object?>{};
|
||||
result['edit'] = edit?.toJson();
|
||||
result['summary'] = summary;
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => jsonEncoder.convert(toJson());
|
||||
|
||||
static bool canParse(Object? obj, LspJsonReporter reporter) {
|
||||
if (obj is Map<String, Object?>) {
|
||||
if (!_canParseWorkspaceEdit(obj, reporter, 'edit',
|
||||
allowsUndefined: false, allowsNull: true)) {
|
||||
return false;
|
||||
}
|
||||
return _canParseString(obj, reporter, 'summary',
|
||||
allowsUndefined: false, allowsNull: true);
|
||||
} else {
|
||||
reporter.reportError('must be of type DartMigrateResult');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static DartMigrateResult fromJson(Map<String, Object?> json) {
|
||||
final editJson = json['edit'];
|
||||
final edit = editJson != null
|
||||
? WorkspaceEdit.fromJson(editJson as Map<String, Object?>)
|
||||
: null;
|
||||
final summaryJson = json['summary'];
|
||||
final summary = summaryJson as String?;
|
||||
return DartMigrateResult(
|
||||
edit: edit,
|
||||
summary: summary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DartTextDocumentSummaryParams implements ToJsonable {
|
||||
static const jsonHandler = LspJsonHandler(
|
||||
DartTextDocumentSummaryParams.canParse,
|
||||
|
||||
Reference in New Issue
Block a user