[DAS] Convert related to cascade fix
R=scheglov@google.com
Fixes https://github.com/dart-lang/sdk/issues/52476
Change-Id: I64d81aaeb5286508e9b03869fd34fc592b0870e6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/390421
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Auto-Submit: Felipe Morschel <fmorschel.dev@gmail.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/convert_related_to_cascade.dart b/pkg/analysis_server/lib/src/services/correction/dart/convert_related_to_cascade.dart
new file mode 100644
index 0000000..818c0f7
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/convert_related_to_cascade.dart
@@ -0,0 +1,150 @@
+// Copyright (c) 2024, 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/correction/fix.dart';
+import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+import 'package:collection/collection.dart';
+import 'package:linter/src/lint_names.dart';
+
+class ConvertRelatedToCascade extends ResolvedCorrectionProducer {
+ final CorrectionProducerContext _context;
+
+ ConvertRelatedToCascade({required super.context}) : _context = context;
+
+ @override
+ CorrectionApplicability get applicability =>
+ // TODO(applicability): comment on why.
+ CorrectionApplicability.singleLocation;
+
+ @override
+ FixKind get fixKind => DartFixKind.CONVERT_RELATED_TO_CASCADE;
+
+ @override
+ Future<void> compute(ChangeBuilder builder) async {
+ var node = this.node;
+ if (node is! ExpressionStatement) return;
+
+ var block = node.parent;
+ if (block is! Block) return;
+
+ var errors = _context.dartFixContext?.resolvedResult.errors
+ .where((error) => error.errorCode.name == LintNames.cascade_invocations)
+ .whereNot((error) =>
+ error.offset == node.offset && error.length == node.length);
+
+ if (errors == null || errors.isEmpty) return;
+
+ var previous = _getPrevious(block, node);
+ var next = _getNext(block, node);
+
+ // Skip if no error has the offset and length of previous or next.
+ if (errors.none((error) =>
+ error.offset == previous?.offset &&
+ error.length == previous?.length) &&
+ errors.none((error) =>
+ error.offset == next?.offset && error.length == next?.length)) {
+ return;
+ }
+
+ // Get the full list of statements with errors that are related to this.
+ List<ExpressionStatement> relatedStatements = [node];
+ while (previous != null && previous is ExpressionStatement) {
+ if (errors.any((error) =>
+ error.offset == previous!.offset &&
+ error.length == previous.length)) {
+ relatedStatements.insert(0, previous);
+ }
+ previous = _getPrevious(block, previous);
+ }
+ while (next != null && next is ExpressionStatement) {
+ if (errors.any((error) =>
+ error.offset == next!.offset && error.length == next.length)) {
+ relatedStatements.add(next);
+ }
+ next = _getNext(block, next);
+ }
+
+ for (var (index, statement) in relatedStatements.indexed) {
+ Token? previousOperator;
+ Token? semicolon;
+ var previous = index > 0
+ ? relatedStatements[index - 1]
+ : _getPrevious(block, statement);
+ if (previous is ExpressionStatement) {
+ semicolon = previous.semicolon;
+ previousOperator = (index == 0)
+ ? _getTargetAndOperator(previous.expression)?.operator
+ : null;
+ } else if (previous is VariableDeclarationStatement) {
+ // Single variable declaration.
+ if (previous.variables.variables.length != 1) {
+ return;
+ }
+ semicolon = previous.endToken;
+ } else {
+ // TODO(fmorschel): Refactor this to collect all changes and apply them
+ // at once.
+ // One unfortunate consequence of this approach is that we might have
+ // already used [builder.addDartFileEdit], and so we could stop with
+ // incomplete changes.
+ // In the future there could be other cases for triggering this fix
+ // other than `ExpressionStatement` and `VariableDeclarationStatement`.
+ return;
+ }
+
+ var expression = statement.expression;
+ var target = _getTargetAndOperator(expression)?.target;
+ if (target == null) return;
+
+ var targetReplacement = expression is CascadeExpression ? '' : '.';
+
+ await builder.addDartFileEdit(file, (builder) {
+ if (previousOperator != null) {
+ builder.addSimpleInsertion(previousOperator.offset, '.');
+ }
+ if (semicolon != null) {
+ builder.addDeletion(range.token(semicolon));
+ }
+ builder.addSimpleReplacement(range.node(target), targetReplacement);
+ });
+ }
+ }
+
+ Statement? _getNext(Block block, Statement statement) {
+ var statements = block.statements;
+ var index = statements.indexOf(statement);
+ return index < (statements.length - 1) ? statements[index + 1] : null;
+ }
+
+ Statement? _getPrevious(Block block, Statement statement) {
+ var statements = block.statements;
+ var index = statements.indexOf(statement);
+ return index > 0 ? statements[index - 1] : null;
+ }
+
+ _TargetAndOperator? _getTargetAndOperator(Expression expression) {
+ if (expression is AssignmentExpression) {
+ var lhs = expression.leftHandSide;
+ if (lhs is PrefixedIdentifier) {
+ return _TargetAndOperator(lhs.prefix, lhs.period);
+ }
+ } else if (expression is MethodInvocation) {
+ return _TargetAndOperator(expression.target, expression.operator);
+ } else if (expression is CascadeExpression) {
+ return _TargetAndOperator(expression.target, null);
+ }
+ return null;
+ }
+}
+
+class _TargetAndOperator {
+ final AstNode? target;
+ final Token? operator;
+ _TargetAndOperator(this.target, this.operator);
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/convert_to_cascade.dart b/pkg/analysis_server/lib/src/services/correction/dart/convert_to_cascade.dart
index df02ecc..bc255e1 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/convert_to_cascade.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/convert_to_cascade.dart
@@ -84,7 +84,7 @@
}
class _TargetAndOperator {
- AstNode? target;
- Token? operator;
+ final AstNode? target;
+ final Token? operator;
_TargetAndOperator(this.target, this.operator);
}
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 797ddeb..b78d905 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -417,6 +417,11 @@
DartFixKindPriority.inFile,
'Convert the quotes and remove escapes everywhere in file',
);
+ static const CONVERT_RELATED_TO_CASCADE = FixKind(
+ 'dart.fix.convert.relatedToCascade',
+ DartFixKindPriority.standard + 1,
+ 'Convert this and related to cascade notation',
+ );
static const CONVERT_TO_BOOL_EXPRESSION = FixKind(
'dart.fix.convert.toBoolExpression',
DartFixKindPriority.standard,
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index ba8c3ac..b0f0419 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -54,6 +54,7 @@
import 'package:analysis_server/src/services/correction/dart/convert_into_is_not.dart';
import 'package:analysis_server/src/services/correction/dart/convert_map_from_iterable_to_for_literal.dart';
import 'package:analysis_server/src/services/correction/dart/convert_quotes.dart';
+import 'package:analysis_server/src/services/correction/dart/convert_related_to_cascade.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_boolean_expression.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_cascade.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_constant_pattern.dart';
@@ -367,6 +368,7 @@
],
LinterLintCode.cascade_invocations: [
ConvertToCascade.new,
+ ConvertRelatedToCascade.new,
],
LinterLintCode.cast_nullable_to_non_nullable: [
AddNullCheck.withoutAssignabilityCheck,
diff --git a/pkg/analysis_server/test/src/services/correction/fix/convert_related_to_cascade_test.dart b/pkg/analysis_server/test/src/services/correction/fix/convert_related_to_cascade_test.dart
new file mode 100644
index 0000000..38253ff
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/convert_related_to_cascade_test.dart
@@ -0,0 +1,209 @@
+// Copyright (c) 2024, 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/correction/fix.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:linter/src/lint_names.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(ConvertRelatedToCascadeTest);
+ });
+}
+
+@reflectiveTest
+class ConvertRelatedToCascadeTest extends FixProcessorLintTest {
+ @override
+ FixKind get kind => DartFixKind.CONVERT_RELATED_TO_CASCADE;
+
+ @override
+ String get lintCode => LintNames.cascade_invocations;
+
+ Future<void> test_declaration_method_method() async {
+ await resolveTestCode('''
+class A {
+ void m() {}
+}
+void f() {
+ final a = A();
+ a.m();
+ a.m();
+}
+''');
+ await assertHasFix('''
+class A {
+ void m() {}
+}
+void f() {
+ final a = A()
+ ..m()
+ ..m();
+}
+''', errorFilter: (error) => error.offset == testCode.indexOf('a.m();'));
+ }
+
+ Future<void> test_method_property_method() async {
+ await resolveTestCode('''
+class A {
+ void m() {}
+ int? x;
+}
+void f(A a) {
+ a.m();
+ a.x = 1;
+ a.m();
+}
+''');
+ await assertHasFix('''
+class A {
+ void m() {}
+ int? x;
+}
+void f(A a) {
+ a..m()
+ ..x = 1
+ ..m();
+}
+''', errorFilter: (error) => error.offset == testCode.indexOf('a.x = 1'));
+ }
+
+ Future<void> test_multipleDeclaration_first_method() async {
+ await resolveTestCode('''
+class A {
+ void m() {}
+}
+void f() {
+ final a = A(), a2 = A();
+ a.m();
+}
+''');
+ await assertNoFix();
+ }
+
+ Future<void> test_multipleDeclaration_last_method() async {
+ await resolveTestCode('''
+class A {
+ void m() {}
+}
+void f() {
+ final a = A(), a2 = A();
+ a2.m();
+}
+''');
+ await assertNoFix();
+ }
+
+ Future<void> test_property_cascadeMethod_cascadeMethod() async {
+ await resolveTestCode('''
+class A {
+ void m() {}
+ int? x;
+}
+
+void f(A a) {
+ a.x = 1;
+ a..m();
+ a..m();
+}
+''');
+ await assertHasFix('''
+class A {
+ void m() {}
+ int? x;
+}
+
+void f(A a) {
+ a..x = 1
+ ..m()
+ ..m();
+}
+''', errorFilter: (error) => error.offset == testCode.indexOf('a..m();'));
+ }
+
+ Future<void> test_property_property_method_method_fisrt() async {
+ await resolveTestCode('''
+class A {
+ void m(int _) {}
+ int? x;
+}
+
+void f(A a) {
+ a..x = 1
+ ..x = 2;
+ a.m(1);
+ a.m(2);
+}
+''');
+ await assertHasFix('''
+class A {
+ void m(int _) {}
+ int? x;
+}
+
+void f(A a) {
+ a..x = 1
+ ..x = 2
+ ..m(1)
+ ..m(2);
+}
+''', errorFilter: (error) => error.offset == testCode.indexOf('a.m(1)'));
+ }
+
+ Future<void> test_property_property_method_method_last() async {
+ await resolveTestCode('''
+class A {
+ void m(int _) {}
+ int? x;
+}
+
+void f(A a) {
+ a..x = 1
+ ..x = 2;
+ a.m(1);
+ a.m(2);
+}
+''');
+ await assertHasFix('''
+class A {
+ void m(int _) {}
+ int? x;
+}
+
+void f(A a) {
+ a..x = 1
+ ..x = 2
+ ..m(1)
+ ..m(2);
+}
+''', errorFilter: (error) => error.offset == testCode.indexOf('a.m(2)'));
+ }
+
+ Future<void> test_property_property_property() async {
+ await resolveTestCode('''
+class A {
+ void m() {}
+ int? x;
+}
+void f(A a) {
+ a.x = 1;
+ a.x = 2;
+ a.x = 3;
+}
+''');
+ await assertHasFix('''
+class A {
+ void m() {}
+ int? x;
+}
+void f(A a) {
+ a..x = 1
+ ..x = 2
+ ..x = 3;
+}
+''', errorFilter: (error) => error.offset == testCode.indexOf('a.x = 2;'));
+ }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index 0244b36..c716d6e 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -68,6 +68,7 @@
import 'convert_into_expression_body_test.dart' as convert_into_expression_body;
import 'convert_into_is_not_test.dart' as convert_into_is_not;
import 'convert_quotes_test.dart' as convert_quotes;
+import 'convert_related_to_cascade_test.dart' as convert_related_to_cascade;
import 'convert_to_block_function_body_test.dart'
as convert_to_block_function_body;
import 'convert_to_boolean_expression_test.dart'
@@ -359,6 +360,7 @@
convert_into_expression_body.main();
convert_into_is_not.main();
convert_quotes.main();
+ convert_related_to_cascade.main();
convert_to_block_function_body.main();
convert_to_boolean_expression.main();
convert_to_cascade.main();