[analysis_server] Dot shorthands: Update CreateMethod fix.

Refactored `CreateMethod` to handle both method invocations and dot shorthand invocations. The code overlaps so a lot of the CL is just re-organizing them into helpers.

Added unit tests.

Bug: https://github.com/dart-lang/sdk/issues/60994
Change-Id: Ie06005b67f0fe5f066ab57a2f0dbc06095597897
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/442729
Commit-Queue: Kallen Tu <kallentu@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Kallen Tu
2025-07-31 13:09:32 -07:00
committed by Commit Queue
parent 6a534c662a
commit 4e432bd60c
2 changed files with 255 additions and 49 deletions
@@ -3,8 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
@@ -88,14 +88,48 @@ class CreateMethod extends ResolvedCorrectionProducer {
}
Future<void> _createMethod(ChangeBuilder builder) async {
if (node is! SimpleIdentifier || node.parent is! MethodInvocation) {
return;
}
if (node is! SimpleIdentifier) return;
_memberName = (node as SimpleIdentifier).name;
var invocation = node.parent as MethodInvocation;
var invocation = node.parent;
switch (invocation) {
case MethodInvocation():
await _createMethodFromMethodInvocation(builder, invocation);
case DotShorthandInvocation():
await _createMethodFromDotShorthandInvocation(builder, invocation);
}
}
Future<void> _createMethodFromDotShorthandInvocation(
ChangeBuilder builder,
DotShorthandInvocation invocation,
) async {
var targetClassElement = computeDotShorthandContextTypeElement(
invocation,
unitResult.libraryElement,
);
if (targetClassElement == null) return;
var targetNode = await _declarationNodeFromElement(targetClassElement);
if (targetNode is! CompilationUnitMember) return;
await _writeMethod(
builder,
invocation,
invocation.argumentList,
targetClassElement.firstFragment,
targetNode,
hasStaticModifier: true,
);
}
Future<void> _createMethodFromMethodInvocation(
ChangeBuilder builder,
MethodInvocation invocation,
) async {
// Prepare environment.
Fragment? targetFragment;
var staticModifier = false;
var hasStaticModifier = false;
CompilationUnitMember? targetNode;
var target = invocation.realTarget;
@@ -117,7 +151,7 @@ class CreateMethod extends ResolvedCorrectionProducer {
if (enclosingMemberParent is CompilationUnitMember &&
enclosingMemberParent is! ExtensionDeclaration) {
targetNode = enclosingMemberParent;
staticModifier = switch (enclosingMember) {
hasStaticModifier = switch (enclosingMember) {
ConstructorDeclaration(:var factoryKeyword) => factoryKeyword != null,
MethodDeclaration(:var isStatic) => isStatic,
FieldDeclaration(
@@ -129,64 +163,72 @@ class CreateMethod extends ResolvedCorrectionProducer {
}
} else {
var targetClassElement = getTargetInterfaceElement(target);
if (targetClassElement == null) {
return;
}
if (targetClassElement == null) return;
targetFragment = targetClassElement.firstFragment;
if (targetClassElement.library.isInSdk) {
return;
}
// Prepare target ClassDeclaration.
if (targetClassElement is MixinElement) {
var fragment = targetClassElement.firstFragment;
targetNode = await getMixinDeclaration(fragment);
} else if (targetClassElement is ClassElement) {
var fragment = targetClassElement.firstFragment;
targetNode = await getClassDeclaration(fragment);
} else if (targetClassElement is ExtensionTypeElement) {
var fragment = targetClassElement.firstFragment;
targetNode = await getExtensionTypeDeclaration(fragment);
} else if (targetClassElement is EnumElement) {
var fragment = targetClassElement.firstFragment;
targetNode = await getEnumDeclaration(fragment);
}
if (targetNode == null) {
return;
}
targetNode = await _declarationNodeFromElement(targetClassElement);
if (targetNode == null) return;
// Maybe static.
if (target is Identifier) {
staticModifier =
hasStaticModifier =
target.element?.kind == ElementKind.CLASS ||
target.element?.kind == ElementKind.ENUM ||
target.element?.kind == ElementKind.EXTENSION_TYPE ||
target.element?.kind == ElementKind.MIXIN;
}
// Use different utils.
var targetPath = targetFragment.libraryFragment!.source.fullName;
var targetResolveResult = await unitResult.session.getResolvedUnit(
targetPath,
);
if (targetResolveResult is! ResolvedUnitResult) {
return;
}
}
await _writeMethod(
builder,
invocation,
invocation.argumentList,
targetFragment,
targetNode,
hasStaticModifier: hasStaticModifier,
);
}
Future<CompilationUnitMember?> _declarationNodeFromElement(
InterfaceElement element,
) async {
if (element.library.isInSdk) return null;
if (element is MixinElement) {
var fragment = element.firstFragment;
return await getMixinDeclaration(fragment);
} else if (element is ClassElement) {
var fragment = element.firstFragment;
return await getClassDeclaration(fragment);
} else if (element is ExtensionTypeElement) {
var fragment = element.firstFragment;
return await getExtensionTypeDeclaration(fragment);
} else if (element is EnumElement) {
var fragment = element.firstFragment;
return await getEnumDeclaration(fragment);
}
return null;
}
/// Inserts the new method into the source code.
Future<void> _writeMethod(
ChangeBuilder builder,
Expression invocation,
ArgumentList argumentList,
Fragment? targetFragment,
CompilationUnitMember? targetNode, {
required bool hasStaticModifier,
}) async {
var targetSource = targetFragment?.libraryFragment!.source;
if (targetSource == null) {
return;
}
if (targetSource == null) return;
var targetFile = targetSource.fullName;
// Build method source.
await builder.addDartFileEdit(targetFile, (builder) {
if (targetNode == null) {
return;
}
if (targetNode == null) return;
builder.insertMethod(targetNode, (builder) {
// Maybe 'static'.
if (staticModifier) {
if (hasStaticModifier) {
builder.write('static ');
}
// Append return type.
var type = inferUndefinedExpressionType(invocation);
if (builder.writeType(type, groupName: 'RETURN_TYPE')) {
builder.write(' ');
@@ -197,7 +239,7 @@ class CreateMethod extends ResolvedCorrectionProducer {
builder.write(_memberName);
});
builder.write('(');
builder.writeParametersMatchingArguments(invocation.argumentList);
builder.writeParametersMatchingArguments(argumentList);
builder.write(')');
if (type?.isDartAsyncFuture == true) {
builder.write(' async');
@@ -450,6 +450,25 @@ void f() {
''');
}
Future<void> test_createQualified_emptyClassBody_dotShorthand() async {
await resolveTestCode('''
class A {}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
await assertHasFix('''
class A {
static A myUndefinedMethod() {}
}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
}
Future<void> test_createQualified_fromClass() async {
await resolveTestCode('''
class A {
@@ -468,6 +487,26 @@ void f() {
''');
}
Future<void> test_createQualified_fromClass_dotShorthand() async {
await resolveTestCode('''
class A {
}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
await assertHasFix('''
class A {
static A myUndefinedMethod() {}
}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
}
Future<void> test_createQualified_fromClass_hasOtherMember() async {
await resolveTestCode('''
class A {
@@ -489,6 +528,30 @@ void f() {
''');
}
Future<void>
test_createQualified_fromClass_hasOtherMember_dotShorthand() async {
await resolveTestCode('''
class A {
foo() {}
}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
await assertHasFix('''
class A {
foo() {}
static A myUndefinedMethod() {}
}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
}
Future<void> test_createQualified_fromExtensionType() async {
await resolveTestCode('''
extension type A(String s) {
@@ -507,6 +570,26 @@ void f() {
''');
}
Future<void> test_createQualified_fromExtensionType_dotShorthand() async {
await resolveTestCode('''
extension type A(String s) {
}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
await assertHasFix('''
extension type A(String s) {
static A myUndefinedMethod() {}
}
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
}
Future<void> test_createQualified_fromInstance() async {
await resolveTestCode('''
class A {
@@ -553,6 +636,17 @@ void f() {
await assertNoFix();
}
Future<void> test_createQualified_targetIsFunctionType_dotShorthand() async {
await resolveTestCode('''
typedef A();
void f() {
A a = .myUndefinedMethod();
print(a);
}
''');
await assertNoFix();
}
Future<void> test_createQualified_targetIsUnresolved() async {
await resolveTestCode('''
void f() {
@@ -867,6 +961,33 @@ void test() {
''');
}
Future<void> test_enum_invocation_static_dotShorthand() async {
await resolveTestCode('''
enum E {
e1,
e2;
}
void test() {
E e = .bar();
print(e);
}
''');
await assertHasFix('''
enum E {
e1,
e2;
static E bar() {}
}
void test() {
E e = .bar();
print(e);
}
''');
}
Future<void> test_enum_tearoff() async {
await resolveTestCode('''
enum E {
@@ -1469,6 +1590,15 @@ void f() {
await assertNoFix();
}
Future<void> test_inSDK_dotShorthand() async {
await resolveTestCode('''
List f() {
return .foo();
}
''');
await assertNoFix();
}
Future<void> test_internal_instance_extension() async {
await resolveTestCode('''
extension E on String {
@@ -1558,6 +1688,40 @@ class D {
''', target: '$testPackageLibPath/test2.dart');
}
Future<void>
test_parameterType_differentPrefixInTargetUnit_dotShorthand() async {
var code2 = r'''
import 'test3.dart' as bbb;
export 'test3.dart';
class D {
}
''';
newFile('$testPackageLibPath/test2.dart', code2);
newFile('$testPackageLibPath/test3.dart', r'''
library test3;
class E {}
''');
await resolveTestCode('''
import 'test2.dart' as aaa;
aaa.D f(aaa.E e) {
return .foo(e);
}
''');
await assertHasFix('''
import 'test3.dart' as bbb;
export 'test3.dart';
class D {
static D foo(bbb.E e) {}
}
''', target: '$testPackageLibPath/test2.dart');
}
Future<void> test_parameterType_inTargetUnit() async {
newFile('$testPackageLibPath/test2.dart', r'''
class D {