diff --git a/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart b/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart index 131be1b12aa..73ee0f5e1bc 100644 --- a/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart +++ b/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart @@ -10,10 +10,123 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/src/dart/ast/ast.dart'; import 'package:analyzer/src/dart/ast/extensions.dart'; +import 'package:analyzer/src/dart/resolver/applicable_extensions.dart'; +import 'package:analyzer/src/utilities/extensions/ast.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; import 'package:meta/meta.dart'; +class CreateExtensionGetter extends ResolvedCorrectionProducer { + String _getterName = ''; + + @override + CorrectionApplicability get applicability { + // Not predictably the correct action. + return CorrectionApplicability.singleLocation; + } + + @override + List get fixArguments => [_getterName]; + + @override + FixKind get fixKind => DartFixKind.CREATE_EXTENSION_GETTER; + + @override + Future compute(ChangeBuilder builder) async { + var nameNode = node; + if (nameNode is! SimpleIdentifier) { + return; + } + if (!nameNode.inGetterContext()) { + return; + } + + _getterName = nameNode.name; + + // prepare target + Expression? target; + switch (nameNode.parent) { + case PrefixedIdentifier prefixedIdentifier: + if (prefixedIdentifier.identifier == nameNode) { + target = prefixedIdentifier.prefix; + } + case PropertyAccess propertyAccess: + if (propertyAccess.propertyName == nameNode) { + target = propertyAccess.target; + } + } + if (target == null) { + return; + } + + // We need the type for the extension. + var targetType = target.staticType; + if (targetType == null || + targetType is DynamicType || + targetType is InvalidType) { + return; + } + + // Try to find the type of the field. + var fieldTypeNode = climbPropertyAccess(nameNode); + var fieldType = inferUndefinedExpressionType(fieldTypeNode); + + void writeGetter(DartEditBuilder builder) { + if (fieldType != null) { + builder.writeType(fieldType); + builder.write(' '); + } + builder.write('get $_getterName => '); + builder.addLinkedEdit('VALUE', (builder) { + builder.write('null'); + }); + builder.write(';'); + } + + // Try to add to an existing extension. + for (var existingExtension in unitResult.unit.declarations) { + if (existingExtension is ExtensionDeclaration) { + var element = existingExtension.declaredElement!; + var instantiated = [element].applicableTo( + targetLibrary: libraryElement, + targetType: targetType, + strictCasts: true, + ); + if (instantiated.isNotEmpty) { + await builder.addDartFileEdit(file, (builder) { + builder.insertGetter(existingExtension, (builder) { + writeGetter(builder); + }); + }); + return; + } + } + } + + // The new extension should be added after it. + var enclosingUnitChild = nameNode.enclosingUnitChild; + if (enclosingUnitChild == null) { + return; + } + + // Add a new extension. + await builder.addDartFileEdit(file, (builder) { + builder.addInsertion(enclosingUnitChild.end, (builder) { + builder.writeln(); + builder.writeln(); + builder.write('extension on '); + builder.writeType(targetType); + builder.writeln(' {'); + builder.write(' '); + writeGetter(builder); + builder.writeln(); + builder.write('}'); + }); + }); + } +} + /// Shared implementation that identifies what getter should be added, /// but delegates to the subtypes to produce the fix code. abstract class CreateFieldOrGetter extends ResolvedCorrectionProducer { diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart index 33069c6d76b..3282a633386 100644 --- a/pkg/analysis_server/lib/src/services/correction/fix.dart +++ b/pkg/analysis_server/lib/src/services/correction/fix.dart @@ -681,6 +681,11 @@ class DartFixKind { DartFixKindPriority.DEFAULT, 'Create constructor to call {0}', ); + static const CREATE_EXTENSION_GETTER = FixKind( + 'dart.fix.create.extension.getter', + DartFixKindPriority.DEFAULT - 20, + "Create extension getter '{0}'", + ); static const CREATE_FIELD = FixKind( 'dart.fix.create.field', 49, 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 9f0581dcec8..25c222073b5 100644 --- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart +++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart @@ -1261,6 +1261,7 @@ final _builtInNonLintProducers = >{ CompileTimeErrorCode.UNDEFINED_GETTER: [ ChangeTo.getterOrSetter, CreateClass.new, + CreateExtensionGetter.new, CreateField.new, CreateGetter.new, CreateLocalVariable.new, diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart index a2786b4daad..30340889192 100644 --- a/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart +++ b/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart @@ -10,11 +10,224 @@ import 'fix_processor.dart'; void main() { defineReflectiveSuite(() { + defineReflectiveTests(CreateExtensionGetterTest); defineReflectiveTests(CreateGetterTest); defineReflectiveTests(CreateGetterMixinTest); }); } +@reflectiveTest +class CreateExtensionGetterTest extends FixProcessorTest { + @override + FixKind get kind => DartFixKind.CREATE_EXTENSION_GETTER; + + Future test_contextType() async { + await resolveTestCode(''' +void f() { + // ignore:unused_local_variable + int v = ''.test; +} +'''); + await assertHasFix(''' +void f() { + // ignore:unused_local_variable + int v = ''.test; +} + +extension on String { + int get test => null; +} +'''); + } + + Future test_contextType_no() async { + await resolveTestCode(''' +void f() { + ''.test; +} +'''); + await assertHasFix(''' +void f() { + ''.test; +} + +extension on String { + get test => null; +} +'''); + assertLinkedGroup(change.linkedEditGroups[0], ['null']); + } + + Future test_existingExtension_contextType() async { + await resolveTestCode(''' +void f() { + // ignore:unused_local_variable + int v = ''.test; +} + +extension on String {} +'''); + await assertHasFix(''' +void f() { + // ignore:unused_local_variable + int v = ''.test; +} + +extension on String { + int get test => null; +} +'''); + } + + Future test_existingExtension_generic_matching() async { + await resolveTestCode(''' +void f(List a) { + a.test; +} + +extension E on Iterable {} +'''); + await assertHasFix(''' +void f(List a) { + a.test; +} + +extension E on Iterable { + get test => null; +} +'''); + } + + Future test_existingExtension_generic_notMatching() async { + await resolveTestCode(''' +void f(List a) { + a.test; +} + +extension E on Map {} +'''); + await assertHasFix(''' +void f(List a) { + a.test; +} + +extension on List { + get test => null; +} + +extension E on Map {} +'''); + } + + Future test_existingExtension_hasMethod() async { + await resolveTestCode(''' +void f() { + ''.test; +} + +extension E on String { + // ignore:unused_element + void foo() {} +} +'''); + await assertHasFix(''' +void f() { + ''.test; +} + +extension E on String { + get test => null; + + // ignore:unused_element + void foo() {} +} +'''); + } + + Future test_existingExtension_notGeneric_matching() async { + await resolveTestCode(''' +void f() { + ''.test; +} + +extension on String {} +'''); + await assertHasFix(''' +void f() { + ''.test; +} + +extension on String { + get test => null; +} +'''); + } + + Future test_existingExtension_notGeneric_notMatching() async { + await resolveTestCode(''' +void f() { + ''.test; +} + +extension on int {} +'''); + await assertHasFix(''' +void f() { + ''.test; +} + +extension on String { + get test => null; +} + +extension on int {} +'''); + } + + Future test_parent_nothing() async { + await resolveTestCode(''' +void f() { + test; +} +'''); + await assertNoFix(); + } + + Future test_parent_prefixedIdentifier() async { + await resolveTestCode(''' +void f(String a) { + a.test; +} +'''); + await assertHasFix(''' +void f(String a) { + a.test; +} + +extension on String { + get test => null; +} +'''); + } + + Future test_targetType_hasTypeArguments() async { + await resolveTestCode(''' +void f(List a) { + a.test; +} +'''); + await assertHasFix(''' +void f(List a) { + a.test; +} + +extension on List { + get test => null; +} +'''); + } +} + @reflectiveTest class CreateGetterMixinTest extends FixProcessorTest { @override diff --git a/pkg/analyzer/lib/src/utilities/extensions/ast.dart b/pkg/analyzer/lib/src/utilities/extensions/ast.dart index 522ab764d3f..56317905c15 100644 --- a/pkg/analyzer/lib/src/utilities/extensions/ast.dart +++ b/pkg/analyzer/lib/src/utilities/extensions/ast.dart @@ -41,6 +41,15 @@ extension AstNodeExtension on AstNode { return null; } + AstNode? get enclosingUnitChild { + for (var node in withParents) { + if (node.parent is CompilationUnit) { + return node; + } + } + return null; + } + /// This node and all its parents. Iterable get withParents sync* { var current = this;