Skip to content

Commit f62c82a

Browse files
bwilkersonCommit Queue
authored andcommitted
Add a refactor to convert an unnamed constructor to a named constructor
Let me know whether there are any missed cases that ought to be covered here. There are tests for the rename constructor that cover a lot of the additional cases, so I mostly focused here on tests related to where the refactor is and isn't provided, but that might not be sufficient. Change-Id: I6e0008597cf852b4acc81a96fa564e0e1a414bd9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/489620 Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
1 parent 62e11ac commit f62c82a

8 files changed

Lines changed: 1193 additions & 1 deletion

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analysis_server/src/services/refactoring/framework/refactoring_producer.dart';
6+
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
7+
import 'package:analysis_server/src/services/search/search_engine_internal.dart';
8+
import 'package:analysis_server/src/utilities/extensions/selection.dart';
9+
import 'package:analyzer/dart/element/element.dart';
10+
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
11+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
12+
import 'package:language_server_protocol/protocol_custom_generated.dart';
13+
14+
/// The refactoring that adds a name to an unnamed constructor.
15+
class AddConstructorName extends RefactoringProducer {
16+
static const String commandName = 'dart.refactor.add_constructor_name';
17+
18+
static const String constTitle = 'Add a name to the constructor';
19+
20+
AddConstructorName(super.context);
21+
22+
@override
23+
bool get isExperimental => false;
24+
25+
@override
26+
List<CommandParameter> get parameters => const <CommandParameter>[];
27+
28+
@override
29+
String get title => constTitle;
30+
31+
@override
32+
Future<ComputeStatus> compute(
33+
List<Object?> commandArguments,
34+
ChangeBuilder builder,
35+
) async {
36+
var element = selection?.constructor(mustNotHaveName: true);
37+
if (element == null) {
38+
// This should never happen because `isAvailable` would have returned
39+
// `false`, so this method wouldn't have been called.
40+
return ComputeStatusFailure();
41+
}
42+
43+
var refactoring = _createRefactoring(element);
44+
if (refactoring == null) {
45+
return ComputeStatusFailure();
46+
}
47+
refactoring.newName = _computeName(element);
48+
var status = await refactoring.checkAllConditions();
49+
if (status.hasError) {
50+
return ComputeStatusFailure();
51+
}
52+
await refactoring.createChange(builder: builder);
53+
return ComputeStatusSuccess();
54+
}
55+
56+
@override
57+
bool isAvailable() {
58+
return selection?.constructor(mustNotHaveName: true) != null;
59+
}
60+
61+
/// Compute a name for the new constructor.
62+
String _computeName(ConstructorElement element) {
63+
var enclosingElement = element.enclosingElement;
64+
var usedNames = <String>{};
65+
usedNames.addAll(enclosingElement.constructors.map((c) => c.name ?? ''));
66+
usedNames.addAll(enclosingElement.methods.map((m) => m.name ?? ''));
67+
usedNames.addAll(enclosingElement.fields.map((f) => f.name ?? ''));
68+
var candidate = 'name';
69+
var index = 1;
70+
while (usedNames.contains(candidate)) {
71+
candidate = 'name$index';
72+
index++;
73+
}
74+
return candidate;
75+
}
76+
77+
RenameRefactoring? _createRefactoring(ConstructorElement element) {
78+
var analysisContext = libraryResult.session.analysisContext;
79+
if (analysisContext is! DriverBasedAnalysisContext) {
80+
return null;
81+
}
82+
var driver = analysisContext.driver;
83+
var searchEngine = SearchEngineImpl([driver]);
84+
return RenameRefactoring.create(
85+
RefactoringWorkspace([driver], searchEngine),
86+
unitResult,
87+
element,
88+
);
89+
}
90+
}

pkg/analysis_server/lib/src/services/refactoring/framework/refactoring_processor.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:analysis_server/src/lsp/constants.dart';
66
import 'package:analysis_server/src/services/correction/refactoring_performance.dart';
7+
import 'package:analysis_server/src/services/refactoring/add_constructor_name.dart';
78
import 'package:analysis_server/src/services/refactoring/convert_all_formal_parameters_to_named.dart';
89
import 'package:analysis_server/src/services/refactoring/convert_selected_formal_parameters_to_named.dart';
910
import 'package:analysis_server/src/services/refactoring/framework/refactoring_context.dart';
@@ -19,6 +20,7 @@ typedef RefactoringProducerGenerator =
1920
class RefactoringProcessor {
2021
/// A list of the generators used to produce refactorings.
2122
static const Map<String, RefactoringProducerGenerator> generators = {
23+
AddConstructorName.commandName: AddConstructorName.new,
2224
ConvertAllFormalParametersToNamed.commandName:
2325
ConvertAllFormalParametersToNamed.new,
2426
ConvertSelectedFormalParametersToNamed.commandName:

pkg/analysis_server/lib/src/services/refactoring/legacy/rename_constructor.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ class RenameConstructorRefactoringImpl extends RenameRefactoringImpl {
6161
});
6262
continue;
6363
} else if (coveringParent is ConstructorDeclaration &&
64-
// TODO(scheglov): support primary constructors
6564
coveringParent.typeName!.offset == reference.range.offset) {
6665
await builder.addDartFileEdit(reference.file, (builder) {
6766
_addSuperInvocationToConstructor(
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analysis_server_plugin/src/utilities/selection.dart';
6+
import 'package:analyzer/dart/ast/ast.dart';
7+
import 'package:analyzer/dart/element/element.dart';
8+
9+
extension SelectionExtension on Selection {
10+
/// The end of the selection.
11+
int get end => offset + length;
12+
13+
/// Returns the element of the constructor at the given [selection].
14+
///
15+
/// Returns `null` if
16+
/// - the selection doesn't identify a constructor, or
17+
/// - [mustHaveName] is `true` and the referenced constructor doesn't have a
18+
/// name, or
19+
/// - [mustNotHaveName] is `true` and the referenced constructor has a name.
20+
ConstructorElement? constructor({
21+
bool mustHaveName = false,
22+
bool mustNotHaveName = false,
23+
}) {
24+
var node = coveringNode;
25+
26+
bool meetsRequirements(Object? name) {
27+
if (name == null) {
28+
if (mustHaveName) return false;
29+
} else {
30+
if (mustNotHaveName) return false;
31+
}
32+
return true;
33+
}
34+
35+
if (node is ConstructorDeclaration) {
36+
if (!meetsRequirements(node.name)) return null;
37+
var left =
38+
node.typeName?.offset ??
39+
node.factoryKeyword?.offset ??
40+
node.newKeyword?.offset ??
41+
node.firstTokenAfterCommentAndMetadata.offset;
42+
var right = node.separator?.offset ?? node.parameters.offset;
43+
if (left <= offset && end <= right) {
44+
return node.declaredFragment?.element;
45+
}
46+
} else if (node is SimpleIdentifier || node is FormalParameterList) {
47+
var parent = node.parent;
48+
if (parent is ConstructorDeclaration) {
49+
if (!meetsRequirements(parent.name)) return null;
50+
return parent.declaredFragment?.element;
51+
} else if (parent is ConstructorName) {
52+
if (!meetsRequirements(parent.name)) return null;
53+
return parent.element;
54+
}
55+
} else if (node is PrimaryConstructorDeclaration) {
56+
if (!meetsRequirements(node.constructorName)) return null;
57+
return node.declaredFragment?.element;
58+
} else if (node is PrimaryConstructorName) {
59+
if (!meetsRequirements(node.name)) return null;
60+
var parent = node.parent;
61+
if (parent is PrimaryConstructorDeclaration) {
62+
return parent.declaredFragment?.element;
63+
}
64+
} else if (node is NamedType) {
65+
var parent = node.parent;
66+
if (parent is ConstructorName) {
67+
if (!meetsRequirements(node.name)) return null;
68+
return parent.element;
69+
}
70+
} else if (node is ConstructorName) {
71+
if (!meetsRequirements(node.name)) return null;
72+
return node.element;
73+
}
74+
return null;
75+
}
76+
}

0 commit comments

Comments
 (0)