Skip to content

Commit b2bc791

Browse files
FMorschelCommit Queue
authored and
Commit Queue
committedOct 24, 2024
[DAS] Convert related to cascade fix
[email protected] Fixes #52476 Change-Id: I64d81aaeb5286508e9b03869fd34fc592b0870e6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/390421 Commit-Queue: Konstantin Shcheglov <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Auto-Submit: Felipe Morschel <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]>
1 parent f06e372 commit b2bc791

File tree

6 files changed

+370
-2
lines changed

6 files changed

+370
-2
lines changed
 
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) 2024, 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/fix.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/ast/token.dart';
9+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
10+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
11+
import 'package:analyzer_plugin/utilities/range_factory.dart';
12+
import 'package:collection/collection.dart';
13+
import 'package:linter/src/lint_names.dart';
14+
15+
class ConvertRelatedToCascade extends ResolvedCorrectionProducer {
16+
final CorrectionProducerContext _context;
17+
18+
ConvertRelatedToCascade({required super.context}) : _context = context;
19+
20+
@override
21+
CorrectionApplicability get applicability =>
22+
// TODO(applicability): comment on why.
23+
CorrectionApplicability.singleLocation;
24+
25+
@override
26+
FixKind get fixKind => DartFixKind.CONVERT_RELATED_TO_CASCADE;
27+
28+
@override
29+
Future<void> compute(ChangeBuilder builder) async {
30+
var node = this.node;
31+
if (node is! ExpressionStatement) return;
32+
33+
var block = node.parent;
34+
if (block is! Block) return;
35+
36+
var errors = _context.dartFixContext?.resolvedResult.errors
37+
.where((error) => error.errorCode.name == LintNames.cascade_invocations)
38+
.whereNot((error) =>
39+
error.offset == node.offset && error.length == node.length);
40+
41+
if (errors == null || errors.isEmpty) return;
42+
43+
var previous = _getPrevious(block, node);
44+
var next = _getNext(block, node);
45+
46+
// Skip if no error has the offset and length of previous or next.
47+
if (errors.none((error) =>
48+
error.offset == previous?.offset &&
49+
error.length == previous?.length) &&
50+
errors.none((error) =>
51+
error.offset == next?.offset && error.length == next?.length)) {
52+
return;
53+
}
54+
55+
// Get the full list of statements with errors that are related to this.
56+
List<ExpressionStatement> relatedStatements = [node];
57+
while (previous != null && previous is ExpressionStatement) {
58+
if (errors.any((error) =>
59+
error.offset == previous!.offset &&
60+
error.length == previous.length)) {
61+
relatedStatements.insert(0, previous);
62+
}
63+
previous = _getPrevious(block, previous);
64+
}
65+
while (next != null && next is ExpressionStatement) {
66+
if (errors.any((error) =>
67+
error.offset == next!.offset && error.length == next.length)) {
68+
relatedStatements.add(next);
69+
}
70+
next = _getNext(block, next);
71+
}
72+
73+
for (var (index, statement) in relatedStatements.indexed) {
74+
Token? previousOperator;
75+
Token? semicolon;
76+
var previous = index > 0
77+
? relatedStatements[index - 1]
78+
: _getPrevious(block, statement);
79+
if (previous is ExpressionStatement) {
80+
semicolon = previous.semicolon;
81+
previousOperator = (index == 0)
82+
? _getTargetAndOperator(previous.expression)?.operator
83+
: null;
84+
} else if (previous is VariableDeclarationStatement) {
85+
// Single variable declaration.
86+
if (previous.variables.variables.length != 1) {
87+
return;
88+
}
89+
semicolon = previous.endToken;
90+
} else {
91+
// TODO(fmorschel): Refactor this to collect all changes and apply them
92+
// at once.
93+
// One unfortunate consequence of this approach is that we might have
94+
// already used [builder.addDartFileEdit], and so we could stop with
95+
// incomplete changes.
96+
// In the future there could be other cases for triggering this fix
97+
// other than `ExpressionStatement` and `VariableDeclarationStatement`.
98+
return;
99+
}
100+
101+
var expression = statement.expression;
102+
var target = _getTargetAndOperator(expression)?.target;
103+
if (target == null) return;
104+
105+
var targetReplacement = expression is CascadeExpression ? '' : '.';
106+
107+
await builder.addDartFileEdit(file, (builder) {
108+
if (previousOperator != null) {
109+
builder.addSimpleInsertion(previousOperator.offset, '.');
110+
}
111+
if (semicolon != null) {
112+
builder.addDeletion(range.token(semicolon));
113+
}
114+
builder.addSimpleReplacement(range.node(target), targetReplacement);
115+
});
116+
}
117+
}
118+
119+
Statement? _getNext(Block block, Statement statement) {
120+
var statements = block.statements;
121+
var index = statements.indexOf(statement);
122+
return index < (statements.length - 1) ? statements[index + 1] : null;
123+
}
124+
125+
Statement? _getPrevious(Block block, Statement statement) {
126+
var statements = block.statements;
127+
var index = statements.indexOf(statement);
128+
return index > 0 ? statements[index - 1] : null;
129+
}
130+
131+
_TargetAndOperator? _getTargetAndOperator(Expression expression) {
132+
if (expression is AssignmentExpression) {
133+
var lhs = expression.leftHandSide;
134+
if (lhs is PrefixedIdentifier) {
135+
return _TargetAndOperator(lhs.prefix, lhs.period);
136+
}
137+
} else if (expression is MethodInvocation) {
138+
return _TargetAndOperator(expression.target, expression.operator);
139+
} else if (expression is CascadeExpression) {
140+
return _TargetAndOperator(expression.target, null);
141+
}
142+
return null;
143+
}
144+
}
145+
146+
class _TargetAndOperator {
147+
final AstNode? target;
148+
final Token? operator;
149+
_TargetAndOperator(this.target, this.operator);
150+
}

‎pkg/analysis_server/lib/src/services/correction/dart/convert_to_cascade.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class ConvertToCascade extends ResolvedCorrectionProducer {
8484
}
8585

8686
class _TargetAndOperator {
87-
AstNode? target;
88-
Token? operator;
87+
final AstNode? target;
88+
final Token? operator;
8989
_TargetAndOperator(this.target, this.operator);
9090
}

‎pkg/analysis_server/lib/src/services/correction/fix.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,11 @@ abstract final class DartFixKind {
417417
DartFixKindPriority.inFile,
418418
'Convert the quotes and remove escapes everywhere in file',
419419
);
420+
static const CONVERT_RELATED_TO_CASCADE = FixKind(
421+
'dart.fix.convert.relatedToCascade',
422+
DartFixKindPriority.standard + 1,
423+
'Convert this and related to cascade notation',
424+
);
420425
static const CONVERT_TO_BOOL_EXPRESSION = FixKind(
421426
'dart.fix.convert.toBoolExpression',
422427
DartFixKindPriority.standard,

‎pkg/analysis_server/lib/src/services/correction/fix_internal.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import 'package:analysis_server/src/services/correction/dart/convert_into_block_
5454
import 'package:analysis_server/src/services/correction/dart/convert_into_is_not.dart';
5555
import 'package:analysis_server/src/services/correction/dart/convert_map_from_iterable_to_for_literal.dart';
5656
import 'package:analysis_server/src/services/correction/dart/convert_quotes.dart';
57+
import 'package:analysis_server/src/services/correction/dart/convert_related_to_cascade.dart';
5758
import 'package:analysis_server/src/services/correction/dart/convert_to_boolean_expression.dart';
5859
import 'package:analysis_server/src/services/correction/dart/convert_to_cascade.dart';
5960
import 'package:analysis_server/src/services/correction/dart/convert_to_constant_pattern.dart';
@@ -367,6 +368,7 @@ final _builtInLintProducers = <LintCode, List<ProducerGenerator>>{
367368
],
368369
LinterLintCode.cascade_invocations: [
369370
ConvertToCascade.new,
371+
ConvertRelatedToCascade.new,
370372
],
371373
LinterLintCode.cast_nullable_to_non_nullable: [
372374
AddNullCheck.withoutAssignabilityCheck,
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Copyright (c) 2024, 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/fix.dart';
6+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
7+
import 'package:linter/src/lint_names.dart';
8+
import 'package:test_reflective_loader/test_reflective_loader.dart';
9+
10+
import 'fix_processor.dart';
11+
12+
void main() {
13+
defineReflectiveSuite(() {
14+
defineReflectiveTests(ConvertRelatedToCascadeTest);
15+
});
16+
}
17+
18+
@reflectiveTest
19+
class ConvertRelatedToCascadeTest extends FixProcessorLintTest {
20+
@override
21+
FixKind get kind => DartFixKind.CONVERT_RELATED_TO_CASCADE;
22+
23+
@override
24+
String get lintCode => LintNames.cascade_invocations;
25+
26+
Future<void> test_declaration_method_method() async {
27+
await resolveTestCode('''
28+
class A {
29+
void m() {}
30+
}
31+
void f() {
32+
final a = A();
33+
a.m();
34+
a.m();
35+
}
36+
''');
37+
await assertHasFix('''
38+
class A {
39+
void m() {}
40+
}
41+
void f() {
42+
final a = A()
43+
..m()
44+
..m();
45+
}
46+
''', errorFilter: (error) => error.offset == testCode.indexOf('a.m();'));
47+
}
48+
49+
Future<void> test_method_property_method() async {
50+
await resolveTestCode('''
51+
class A {
52+
void m() {}
53+
int? x;
54+
}
55+
void f(A a) {
56+
a.m();
57+
a.x = 1;
58+
a.m();
59+
}
60+
''');
61+
await assertHasFix('''
62+
class A {
63+
void m() {}
64+
int? x;
65+
}
66+
void f(A a) {
67+
a..m()
68+
..x = 1
69+
..m();
70+
}
71+
''', errorFilter: (error) => error.offset == testCode.indexOf('a.x = 1'));
72+
}
73+
74+
Future<void> test_multipleDeclaration_first_method() async {
75+
await resolveTestCode('''
76+
class A {
77+
void m() {}
78+
}
79+
void f() {
80+
final a = A(), a2 = A();
81+
a.m();
82+
}
83+
''');
84+
await assertNoFix();
85+
}
86+
87+
Future<void> test_multipleDeclaration_last_method() async {
88+
await resolveTestCode('''
89+
class A {
90+
void m() {}
91+
}
92+
void f() {
93+
final a = A(), a2 = A();
94+
a2.m();
95+
}
96+
''');
97+
await assertNoFix();
98+
}
99+
100+
Future<void> test_property_cascadeMethod_cascadeMethod() async {
101+
await resolveTestCode('''
102+
class A {
103+
void m() {}
104+
int? x;
105+
}
106+
107+
void f(A a) {
108+
a.x = 1;
109+
a..m();
110+
a..m();
111+
}
112+
''');
113+
await assertHasFix('''
114+
class A {
115+
void m() {}
116+
int? x;
117+
}
118+
119+
void f(A a) {
120+
a..x = 1
121+
..m()
122+
..m();
123+
}
124+
''', errorFilter: (error) => error.offset == testCode.indexOf('a..m();'));
125+
}
126+
127+
Future<void> test_property_property_method_method_fisrt() async {
128+
await resolveTestCode('''
129+
class A {
130+
void m(int _) {}
131+
int? x;
132+
}
133+
134+
void f(A a) {
135+
a..x = 1
136+
..x = 2;
137+
a.m(1);
138+
a.m(2);
139+
}
140+
''');
141+
await assertHasFix('''
142+
class A {
143+
void m(int _) {}
144+
int? x;
145+
}
146+
147+
void f(A a) {
148+
a..x = 1
149+
..x = 2
150+
..m(1)
151+
..m(2);
152+
}
153+
''', errorFilter: (error) => error.offset == testCode.indexOf('a.m(1)'));
154+
}
155+
156+
Future<void> test_property_property_method_method_last() async {
157+
await resolveTestCode('''
158+
class A {
159+
void m(int _) {}
160+
int? x;
161+
}
162+
163+
void f(A a) {
164+
a..x = 1
165+
..x = 2;
166+
a.m(1);
167+
a.m(2);
168+
}
169+
''');
170+
await assertHasFix('''
171+
class A {
172+
void m(int _) {}
173+
int? x;
174+
}
175+
176+
void f(A a) {
177+
a..x = 1
178+
..x = 2
179+
..m(1)
180+
..m(2);
181+
}
182+
''', errorFilter: (error) => error.offset == testCode.indexOf('a.m(2)'));
183+
}
184+
185+
Future<void> test_property_property_property() async {
186+
await resolveTestCode('''
187+
class A {
188+
void m() {}
189+
int? x;
190+
}
191+
void f(A a) {
192+
a.x = 1;
193+
a.x = 2;
194+
a.x = 3;
195+
}
196+
''');
197+
await assertHasFix('''
198+
class A {
199+
void m() {}
200+
int? x;
201+
}
202+
void f(A a) {
203+
a..x = 1
204+
..x = 2
205+
..x = 3;
206+
}
207+
''', errorFilter: (error) => error.offset == testCode.indexOf('a.x = 2;'));
208+
}
209+
}

0 commit comments

Comments
 (0)