Skip to content

Commit 91b26a3

Browse files
mosuemCommit Queue
authored andcommitted
Add "Bind To Fields" assist
So this one has been bugging me for a while - I usually add an argument to a constructor somewhere, then use the wonderful "Create constructor". Then it gets ugly, as I have to manually make the new parameter a field. This CL solves this by adding a bind to field assist. Change-Id: I76f466aa74e539f62014b01251ad723f5c7d1f2b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/448202 Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Moritz Sümmermann <[email protected]>
1 parent dbce53f commit 91b26a3

File tree

7 files changed

+733
-0
lines changed

7 files changed

+733
-0
lines changed

pkg/analysis_server/lib/src/services/correction/assist.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ abstract final class DartAssistKind {
3636
DartAssistKindPriority.default_,
3737
'Assign value to new local variable',
3838
);
39+
static const bindAllToFields = AssistKind(
40+
'dart.assist.bindAllToFields',
41+
DartAssistKindPriority.default_,
42+
'Bind all parameters to fields',
43+
);
44+
static const bindToField = AssistKind(
45+
'dart.assist.bindToField',
46+
DartAssistKindPriority.default_,
47+
'Bind parameter to field',
48+
);
3949
static const convertClassToEnum = AssistKind(
4050
'dart.assist.convert.classToEnum',
4151
DartAssistKindPriority.default_,

pkg/analysis_server/lib/src/services/correction/assist_internal.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:analysis_server/src/services/correction/dart/add_late.dart';
88
import 'package:analysis_server/src/services/correction/dart/add_return_type.dart';
99
import 'package:analysis_server/src/services/correction/dart/add_type_annotation.dart';
1010
import 'package:analysis_server/src/services/correction/dart/assign_to_local_variable.dart';
11+
import 'package:analysis_server/src/services/correction/dart/bind_all_to_fields.dart';
12+
import 'package:analysis_server/src/services/correction/dart/bind_to_field.dart';
1113
import 'package:analysis_server/src/services/correction/dart/convert_add_all_to_spread.dart';
1214
import 'package:analysis_server/src/services/correction/dart/convert_class_to_enum.dart';
1315
import 'package:analysis_server/src/services/correction/dart/convert_class_to_mixin.dart';
@@ -88,6 +90,8 @@ const Set<ProducerGenerator> _builtInGenerators = {
8890
AddReturnType.new,
8991
AddTypeAnnotation.bulkFixable,
9092
AssignToLocalVariable.new,
93+
BindAllToFields.new,
94+
BindToField.new,
9195
ConvertAddAllToSpread.new,
9296
ConvertClassToEnum.new,
9397
ConvertClassToMixin.new,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2025, 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/correction/assist.dart';
6+
import 'package:analysis_server/src/services/correction/dart/bind_to_field.dart'
7+
show BindToField;
8+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
9+
import 'package:analyzer/dart/ast/ast.dart';
10+
import 'package:analyzer/src/dart/ast/ast.dart';
11+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
12+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
13+
14+
import 'create_constructor.dart';
15+
16+
/// Binds a constructor parameter to a newly created field.
17+
///
18+
/// This assist is useful for simplifying constructor declarations by
19+
/// automatically converting a regular parameter into a `this.` field formal
20+
/// parameter and declaring the corresponding field. This matches a workflow
21+
/// with the [CreateConstructor] assist.
22+
class BindAllToFields extends ResolvedCorrectionProducer {
23+
BindAllToFields({required super.context});
24+
25+
@override
26+
CorrectionApplicability get applicability =>
27+
CorrectionApplicability.singleLocation;
28+
29+
@override
30+
AssistKind get assistKind => DartAssistKind.bindAllToFields;
31+
32+
@override
33+
Future<void> compute(ChangeBuilder builder) async {
34+
var parameterList = node.thisOrAncestorOfType<FormalParameterList>();
35+
if (parameterList != null) {
36+
for (var parameter in parameterList.parameters) {
37+
await BindToField.tryReplacingParameter(
38+
file,
39+
builder,
40+
parameter,
41+
libraryElement2,
42+
);
43+
}
44+
}
45+
}
46+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) 2025, 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/correction/assist.dart';
6+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
7+
import 'package:analyzer/dart/ast/ast.dart';
8+
import 'package:analyzer/dart/element/element.dart';
9+
import 'package:analyzer/dart/element/type.dart';
10+
import 'package:analyzer/source/source_range.dart';
11+
import 'package:analyzer/src/dart/ast/ast.dart';
12+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
13+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
14+
import 'package:analyzer_plugin/utilities/range_factory.dart';
15+
import 'package:collection/collection.dart';
16+
17+
import 'create_constructor.dart';
18+
19+
/// Binds a constructor parameter to a newly created field.
20+
///
21+
/// This assist is useful for simplifying constructor declarations by
22+
/// automatically converting a regular parameter into a `this.` field formal
23+
/// parameter and declaring the corresponding field. This matches a workflow
24+
/// with the [CreateConstructor] assist.
25+
class BindToField extends ResolvedCorrectionProducer {
26+
BindToField({required super.context});
27+
28+
@override
29+
CorrectionApplicability get applicability =>
30+
CorrectionApplicability.singleLocation;
31+
32+
@override
33+
AssistKind get assistKind => DartAssistKind.bindAllToFields;
34+
35+
@override
36+
Future<void> compute(ChangeBuilder builder) async {
37+
var parameter = node.thisOrAncestorOfType<FormalParameter>();
38+
if (parameter != null) {
39+
// Don't propose an assist for super parameters and super parameters with
40+
// a default value because they can't be bound to a field.
41+
await tryReplacingParameter(file, builder, parameter, libraryElement2);
42+
}
43+
}
44+
45+
static Future<void> tryReplacingParameter(
46+
String file,
47+
ChangeBuilder builder,
48+
FormalParameter parameter,
49+
LibraryElement libraryElement2,
50+
) async {
51+
if (parameter is SuperFormalParameter) {
52+
return;
53+
}
54+
if (parameter is DefaultFormalParameter &&
55+
parameter.parameter is SuperFormalParameter) {
56+
return;
57+
}
58+
if (parameter is FieldFormalParameter) {
59+
return;
60+
}
61+
await _replaceParameterWithThis(file, builder, parameter, libraryElement2);
62+
}
63+
64+
static SourceRange _replaceable(FormalParameter parameter) {
65+
return switch (parameter) {
66+
SimpleFormalParameter() => range.startEnd(
67+
parameter.type ?? parameter.keyword ?? parameter.name!,
68+
parameter,
69+
),
70+
DefaultFormalParameter() => _replaceable(parameter.parameter),
71+
FunctionTypedFormalParameter() => range.node(parameter),
72+
// Should not happen, as these are excluded, but better to not crash.
73+
FieldFormalParameter() || SuperFormalParameter() => range.node(parameter),
74+
};
75+
}
76+
77+
static Future<void> _replaceParameterWithThis(
78+
String file,
79+
ChangeBuilder builder,
80+
FormalParameter parameter,
81+
LibraryElement libraryElement2,
82+
) async {
83+
var constructor = parameter.thisOrAncestorOfType<ConstructorDeclaration>();
84+
if (constructor == null) {
85+
return;
86+
}
87+
if (constructor.childEntities.any(
88+
(element) => element is RedirectingConstructorInvocation,
89+
)) {
90+
return;
91+
}
92+
if (constructor.factoryKeyword != null) {
93+
return;
94+
}
95+
var container = constructor.thisOrAncestorOfType<CompilationUnitMember>();
96+
if (container == null) {
97+
return;
98+
}
99+
if (container is! ClassDeclaration && container is! EnumDeclaration) {
100+
return;
101+
}
102+
if (container is ClassDeclaration &&
103+
container.members.any(
104+
(member) => switch (member) {
105+
ConstructorDeclaration() => false,
106+
FieldDeclaration() => false,
107+
MethodDeclaration() => member.name.lexeme == parameter.name?.lexeme,
108+
},
109+
)) {
110+
return;
111+
}
112+
if (container is EnumDeclaration &&
113+
container.constants.any(
114+
(constant) => constant.name.lexeme == parameter.name?.lexeme,
115+
)) {
116+
return;
117+
}
118+
119+
await builder.addDartFileEdit(file, (builder) {
120+
var name = parameter.name;
121+
if (name != null) {
122+
DartType? type = parameter.declaredFragment?.element.type;
123+
var fixType = _whetherToCreateNewField(
124+
container,
125+
parameter,
126+
type,
127+
libraryElement2,
128+
);
129+
if (fixType != _FixType.noop) {
130+
builder.addSimpleReplacement(
131+
_replaceable(parameter),
132+
'this.${name.lexeme}',
133+
);
134+
}
135+
if (fixType == _FixType.replaceWithThisAndNewField) {
136+
builder.insertField(container, (builder) {
137+
var isFinal = constructor.constKeyword != null || parameter.isFinal;
138+
builder.writeFieldDeclaration(
139+
name.lexeme,
140+
isFinal: isFinal,
141+
nameGroupName: 'NAME',
142+
type: isFinal && type is DynamicType ? null : type,
143+
typeGroupName: 'TYPE',
144+
);
145+
});
146+
}
147+
}
148+
});
149+
}
150+
151+
static _FixType _whetherToCreateNewField(
152+
CompilationUnitMember container,
153+
FormalParameter parameter,
154+
DartType? type,
155+
LibraryElement libraryElement2,
156+
) {
157+
if (container is ClassDeclaration) {
158+
var fieldWithSameName = container.members
159+
.whereType<FieldDeclaration>()
160+
.map(
161+
(member) => member.fields.variables.firstWhereOrNull(
162+
(variable) => variable.name.lexeme == parameter.name?.lexeme,
163+
),
164+
)
165+
.firstOrNull;
166+
if (fieldWithSameName != null) {
167+
var fieldType = fieldWithSameName.declaredFragment?.element.type;
168+
if (type != null && fieldType != null) {
169+
if (libraryElement2.typeSystem.isAssignableTo(type, fieldType)) {
170+
return _FixType.replaceWithThis;
171+
} else {
172+
return _FixType.noop;
173+
}
174+
} else {
175+
return _FixType.replaceWithThis;
176+
}
177+
} else {
178+
return _FixType.replaceWithThisAndNewField;
179+
}
180+
}
181+
return _FixType.replaceWithThisAndNewField;
182+
}
183+
}
184+
185+
enum _FixType { replaceWithThis, replaceWithThisAndNewField, noop }
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) 2025, 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/correction/assist.dart';
6+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
7+
import 'package:test_reflective_loader/test_reflective_loader.dart';
8+
9+
import 'assist_processor.dart';
10+
11+
void main() {
12+
defineReflectiveSuite(() {
13+
defineReflectiveTests(BindToFieldTest);
14+
});
15+
}
16+
17+
@reflectiveTest
18+
class BindToFieldTest extends AssistProcessorTest {
19+
@override
20+
AssistKind get kind => DartAssistKind.bindAllToFields;
21+
22+
Future<void> test_typed_multiple_constructor_parameters() async {
23+
await resolveTestCode('''
24+
class A {
25+
A(int ^i, String s);
26+
}
27+
''');
28+
await assertHasAssist('''
29+
class A {
30+
int i;
31+
32+
String s;
33+
34+
A(this.i, this.s);
35+
}
36+
''');
37+
}
38+
39+
Future<void> test_typed_super_default_mixed() async {
40+
await resolveTestCode('''
41+
class A extends B {
42+
A([super.^i = 42, int k = 4]);
43+
}
44+
45+
class B {
46+
B(int i);
47+
}
48+
''');
49+
await assertHasAssist('''
50+
class A extends B {
51+
int k;
52+
53+
A([super.^i = 42, this.k = 4]);
54+
}
55+
56+
class B {
57+
B(int i);
58+
}
59+
''');
60+
}
61+
}

0 commit comments

Comments
 (0)