diff --git a/pkg/analysis_server/lib/src/services/correction/dart/ambiguous_import_fix.dart b/pkg/analysis_server/lib/src/services/correction/dart/ambiguous_import_fix.dart new file mode 100644 index 000000000000..c0b5b6881147 --- /dev/null +++ b/pkg/analysis_server/lib/src/services/correction/dart/ambiguous_import_fix.dart @@ -0,0 +1,336 @@ +// Copyright (c) 2025, 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/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element2.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer/src/dart/ast/extensions.dart'; +import 'package:analyzer/src/utilities/extensions/results.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'; + +class AmbiguousImportFix extends MultiCorrectionProducer { + AmbiguousImportFix({required super.context}); + + @override + Future> get producers async { + var node = this.node; + Element2? element; + String? prefix; + if (node is NamedType) { + element = node.element2; + prefix = node.importPrefix?.name.lexeme; + } else if (node is SimpleIdentifier) { + element = node.element; + if (node.parent case PrefixedIdentifier(prefix: var currentPrefix)) { + prefix = currentPrefix.name; + } + } + if (element is! MultiplyDefinedElement2) { + return const []; + } + var conflictingElements = element.conflictingElements2; + var name = element.name3; + if (name == null || name.isEmpty) { + return const []; + } + + var (unit, importDirectives, uris) = _getImportDirectives( + libraryResult, + unitResult, + conflictingElements, + name, + prefix, + ); + + // If we have multiple imports of the same library, then we won't fix it. + if (uris.length != uris.toSet().length) { + return const []; + } + + if (unit == null || importDirectives.isEmpty || uris.isEmpty) { + return const []; + } + + var producers = []; + var thisContext = CorrectionProducerContext.createResolved( + libraryResult: libraryResult, + unitResult: unit, + applyingBulkFixes: applyingBulkFixes, + dartFixContext: context.dartFixContext, + ); + + for (var uri in uris) { + var directives = + importDirectives + .whereNot((directive) => directive.uri.stringValue == uri) + .toList(); + producers.add( + _ImportAddHide(name, uri, prefix, directives, context: thisContext), + ); + producers.add( + _ImportRemoveShow(name, uri, prefix, directives, context: thisContext), + ); + } + return producers; + } + + /// Returns [ImportDirective]s that import the given [conflictingElements] + /// into [unitResult] and the set of uris (String) that represent each of the + /// import directives. + /// + /// The uris and the import directives are both returned so that we can + /// run the fix for a certain uri on all of the other import directives. + /// + /// The resulting [ResolvedUnitResult?] is the unit that contains the import + /// directives. Usually this is the unit that contains the conflicting + /// element, but it could be a parent unit if the conflicting element is + /// a part file and the relevant imports are in an upstream file in the + /// part hierarchy (enhanced parts). + (ResolvedUnitResult?, List, List) + _getImportDirectives( + ResolvedLibraryResult libraryResult, + ResolvedUnitResult? unitResult, + List conflictingElements, + String name, + String? prefix, + ) { + // The uris of all import directives that import the conflicting elements. + var uris = []; + // The import directives that import the conflicting elements. + var importDirectives = []; + + // Search in each unit up the chain for related imports. + while (unitResult is ResolvedUnitResult) { + for (var conflictingElement in conflictingElements) { + // Find all ImportDirective that import this library in this unit + // and have the same prefix. + for (var directive + in unitResult.unit.directives.whereType()) { + var libraryImport = directive.libraryImport; + if (libraryImport == null) { + continue; + } + + // If the prefix is different, then this directive is not relevant. + if (directive.prefix?.name != prefix) { + continue; + } + + // If this library is imported directly or if the directive exports + // the library for this element. + var element = + prefix != null + ? libraryImport.namespace.getPrefixed2(prefix, name) + : libraryImport.namespace.get2(name); + if (element == conflictingElement) { + var uri = directive.uri.stringValue; + if (uri != null) { + uris.add(uri); + importDirectives.add(directive); + } + } + } + } + + if (importDirectives.isNotEmpty) { + break; + } + + // We continue up the chain. + unitResult = libraryResult.parentUnitOf(unitResult); + } + + return (unitResult, importDirectives, uris); + } +} + +class _ImportAddHide extends ResolvedCorrectionProducer { + final List importDirectives; + final String uri; + final String? prefix; + final String _elementName; + + _ImportAddHide( + this._elementName, + this.uri, + this.prefix, + this.importDirectives, { + required super.context, + }); + + @override + CorrectionApplicability get applicability => + // TODO(applicability): comment on why. + CorrectionApplicability + .singleLocation; + + @override + List get fixArguments { + var prefix = ''; + if (!this.prefix.isEmptyOrNull) { + prefix = ' as ${this.prefix}'; + } + return [_elementName, uri, prefix]; + } + + @override + FixKind get fixKind => DartFixKind.IMPORT_LIBRARY_HIDE; + + @override + Future compute(ChangeBuilder builder) async { + if (_elementName.isEmpty || uri.isEmpty) { + return; + } + + var hideCombinators = + <({ImportDirective directive, List hideList})>[]; + + for (var directive in importDirectives) { + var show = directive.combinators.whereType().firstOrNull; + // If there is an import with a show combinator, then we don't want to + // deal with this case here. + if (show != null) { + return; + } + var hide = directive.combinators.whereType().toList(); + hideCombinators.add((directive: directive, hideList: hide)); + } + + await builder.addDartFileEdit(file, (builder) { + for (var (:directive, :hideList) in hideCombinators) { + for (var hide in hideList) { + var allNames = [ + ...hide.hiddenNames.map((name) => name.name), + _elementName, + ]; + if (_sortCombinators) { + allNames.sort(); + } + var combinator = 'hide ${allNames.join(', ')}'; + builder.addSimpleReplacement(range.node(hide), combinator); + } + if (hideList.isEmpty) { + var hideCombinator = ' hide $_elementName'; + builder.addSimpleInsertion(directive.end - 1, hideCombinator); + } + } + }); + } +} + +class _ImportRemoveShow extends ResolvedCorrectionProducer { + final List importDirectives; + final String _elementName; + final String uri; + final String? prefix; + + _ImportRemoveShow( + this._elementName, + this.uri, + this.prefix, + this.importDirectives, { + required super.context, + }); + + @override + CorrectionApplicability get applicability => + // TODO(applicability): comment on why. + CorrectionApplicability + .singleLocation; + + @override + List get fixArguments { + var prefix = ''; + if (!this.prefix.isEmptyOrNull) { + prefix = ' as ${this.prefix}'; + } + return [_elementName, uri, prefix]; + } + + @override + FixKind get fixKind => DartFixKind.IMPORT_LIBRARY_REMOVE_SHOW; + + @override + Future compute(ChangeBuilder builder) async { + if (_elementName.isEmpty || uri.isEmpty) { + return; + } + + var showCombinators = + < + ({ + ImportDirective directive, + List showList, + List hideList, + }) + >[]; + + for (var directive in importDirectives) { + var show = directive.combinators.whereType().toList(); + var hide = directive.combinators.whereType().toList(); + // If there is no show combinator, then we don't want to deal with this + // case here. + if (show.isEmpty) { + return; + } + showCombinators.add(( + directive: directive, + showList: show, + hideList: hide, + )); + } + + await builder.addDartFileEdit(file, (builder) { + for (var (:directive, :showList, :hideList) in showCombinators) { + var noShow = true; + for (var show in showList) { + var allNames = [ + ...show.shownNames + .map((name) => name.name) + .where((name) => name != _elementName), + ]; + if (_sortCombinators) { + allNames.sort(); + } + if (allNames.isEmpty) { + builder.addDeletion(SourceRange(show.offset - 1, show.length + 1)); + } else { + noShow = false; + var combinator = 'show ${allNames.join(', ')}'; + var range = SourceRange(show.offset, show.length); + builder.addSimpleReplacement(range, combinator); + } + } + if (noShow) { + if (hideList.isEmpty) { + var hideCombinator = ' hide $_elementName'; + builder.addSimpleInsertion(directive.end - 1, hideCombinator); + } + for (var hide in hideList) { + var allNames = [ + ...hide.hiddenNames.map((name) => name.name), + _elementName, + ]; + if (_sortCombinators) { + allNames.sort(); + } + var combinator = 'hide ${allNames.join(', ')}'; + builder.addSimpleReplacement(range.node(hide), combinator); + } + } + } + }); + } +} + +extension on ResolvedCorrectionProducer { + bool get _sortCombinators => + getCodeStyleOptions(unitResult.file).sortCombinators; +} diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml index 125dc93c4047..462fb786d3de 100644 --- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml +++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml @@ -156,12 +156,11 @@ CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE: CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO: status: hasFix CompileTimeErrorCode.AMBIGUOUS_IMPORT: - status: needsFix + status: hasFix notes: |- - 1. For each imported name, add a fix to hide the name. - 2. For each imported name, add a fix to add a prefix. We wouldn't be able to - add the prefix everywhere, but could add it wherever the name was already - unambiguous. + For each imported name, add a fix to add a prefix. We wouldn't be able to + add the prefix everywhere, but could add it wherever the name was already + unambiguous. CompileTimeErrorCode.AMBIGUOUS_SET_OR_MAP_LITERAL_BOTH: status: noFix notes: |- diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart index ae23d25754fc..032727841a9b 100644 --- a/pkg/analysis_server/lib/src/services/correction/fix.dart +++ b/pkg/analysis_server/lib/src/services/correction/fix.dart @@ -849,6 +849,11 @@ abstract final class DartFixKind { DartFixKindPriority.standard + 5, "Update library '{0}' import", ); + static const IMPORT_LIBRARY_HIDE = FixKind( + 'dart.fix.import.libraryHide', + DartFixKindPriority.standard, + "Hide others to use '{0}' from '{1}'{2}", + ); static const IMPORT_LIBRARY_PREFIX = FixKind( 'dart.fix.import.libraryPrefix', DartFixKindPriority.standard + 5, @@ -914,6 +919,11 @@ abstract final class DartFixKind { DartFixKindPriority.standard + 1, "Import library '{0}' with 'show'", ); + static const IMPORT_LIBRARY_REMOVE_SHOW = FixKind( + 'dart.fix.import.libraryRemoveShow', + DartFixKindPriority.standard - 1, + "Remove show to use '{0}' from '{1}'{2}", + ); static const IMPORT_LIBRARY_SDK = FixKind( 'dart.fix.import.librarySdk', DartFixKindPriority.standard + 4, @@ -924,16 +934,16 @@ abstract final class DartFixKind { DartFixKindPriority.standard + 4, "Import library '{0}' with prefix '{1}'", ); - static const IMPORT_LIBRARY_SDK_PREFIXED_SHOW = FixKind( - 'dart.fix.import.librarySdkPrefixedShow', - DartFixKindPriority.standard + 4, - "Import library '{0}' with prefix '{1}' and 'show'", - ); static const IMPORT_LIBRARY_SDK_SHOW = FixKind( 'dart.fix.import.librarySdkShow', DartFixKindPriority.standard + 4, "Import library '{0}' with 'show'", ); + static const IMPORT_LIBRARY_SDK_PREFIXED_SHOW = FixKind( + 'dart.fix.import.librarySdkPrefixedShow', + DartFixKindPriority.standard + 4, + "Import library '{0}' with prefix '{1}' and 'show'", + ); static const INLINE_INVOCATION = FixKind( 'dart.fix.inlineInvocation', DartFixKindPriority.standard - 20, 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 304378415eee..5ab867478296 100644 --- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart +++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart @@ -38,6 +38,7 @@ import 'package:analysis_server/src/services/correction/dart/add_super_parameter import 'package:analysis_server/src/services/correction/dart/add_switch_case_break.dart'; import 'package:analysis_server/src/services/correction/dart/add_trailing_comma.dart'; import 'package:analysis_server/src/services/correction/dart/add_type_annotation.dart'; +import 'package:analysis_server/src/services/correction/dart/ambiguous_import_fix.dart'; import 'package:analysis_server/src/services/correction/dart/change_argument_name.dart'; import 'package:analysis_server/src/services/correction/dart/change_to.dart'; import 'package:analysis_server/src/services/correction/dart/change_to_nearest_precise_value.dart'; @@ -550,6 +551,7 @@ final _builtInNonLintMultiProducers = { CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE: [ AddExtensionOverride.new, ], + CompileTimeErrorCode.AMBIGUOUS_IMPORT: [AmbiguousImportFix.new], CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE: [DataDriven.new], CompileTimeErrorCode.CAST_TO_NON_TYPE: [ DataDriven.new, diff --git a/pkg/analysis_server/test/src/services/correction/fix/ambiguous_import_fix_test.dart b/pkg/analysis_server/test/src/services/correction/fix/ambiguous_import_fix_test.dart new file mode 100644 index 000000000000..c75809892237 --- /dev/null +++ b/pkg/analysis_server/test/src/services/correction/fix/ambiguous_import_fix_test.dart @@ -0,0 +1,931 @@ +// Copyright (c) 2025, 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/src/error/codes.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(ImportAddHideTest); + defineReflectiveTests(ImportRemoveShowTest); + }); +} + +@reflectiveTest +class ImportAddHideTest extends FixProcessorTest { + @override + FixKind get kind => DartFixKind.IMPORT_LIBRARY_HIDE; + + Future test_double() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +'''); + await assertHasFixesWithoutApplying( + expectedNumberOfFixesForKind: 2, + matchFixMessages: [ + "Hide others to use 'N' from 'lib1.dart'", + "Hide others to use 'N' from 'lib2.dart'", + ], + ); + await assertHasFix(''' +import 'lib1.dart' hide N; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib2.dart'"); + await assertHasFix(''' +import 'lib1.dart'; +import 'lib2.dart' hide N; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib1.dart'"); + } + + Future test_double_aliased() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' as i; +import 'lib2.dart' as i; + +void f(i.N? n) { + print(n); +} +'''); + await assertHasFixesWithoutApplying( + expectedNumberOfFixesForKind: 2, + matchFixMessages: [ + "Hide others to use 'N' from 'lib1.dart' as i", + "Hide others to use 'N' from 'lib2.dart' as i", + ], + ); + await assertHasFix(''' +import 'lib1.dart' as i; +import 'lib2.dart' as i hide N; + +void f(i.N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib1.dart' as i"); + await assertHasFix(''' +import 'lib1.dart' as i hide N; +import 'lib2.dart' as i; + +void f(i.N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib2.dart' as i"); + } + + Future test_double_constant() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +const foo = 0;'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +const foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib2.dart'; + +void f() { + print(foo); +} +'''); + await assertHasFix(''' +import 'lib1.dart'; +import 'lib2.dart' hide foo; + +void f() { + print(foo); +} +''', matchFixMessage: "Hide others to use 'foo' from 'lib1.dart'"); + } + + Future test_double_doubleExportedByImport() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +mixin M {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +mixin M {}'''); + newFile(join(testPackageLibPath, 'lib3.dart'), ''' +export 'lib2.dart';'''); + newFile(join(testPackageLibPath, 'lib4.dart'), ''' +export 'lib3.dart';'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib4.dart'; + +class C with M {} +'''); + await assertHasFix( + ''' +import 'lib1.dart' hide M; +import 'lib4.dart'; + +class C with M {} +''', + matchFixMessage: "Hide others to use 'M' from 'lib4.dart'", + errorFilter: (error) { + return error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT; + }, + ); + } + + Future test_double_equal_importUris() async { + // https://github.com/dart-lang/sdk/issues/56830#issuecomment-2573945155 + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +var foo = 0; +var bar = 0; +var baz = 0; +'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +var foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart' hide bar; +import 'lib1.dart' hide baz; +import 'lib2.dart'; + +void f() { + print(bar); + print(baz); + print(foo); +} +'''); + await assertNoFix(); + } + + Future test_double_exportedByImport() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +mixin M {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +mixin M {}'''); + newFile(join(testPackageLibPath, 'lib3.dart'), ''' +export 'lib2.dart';'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib3.dart'; + +class C with M {} +'''); + await assertHasFix( + ''' +import 'lib1.dart' hide M; +import 'lib3.dart'; + +class C with M {} +''', + matchFixMessage: "Hide others to use 'M' from 'lib3.dart'", + errorFilter: (error) { + return error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT; + }, + ); + } + + Future test_double_extension() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +extension E on int { + bool get isDivisibleByThree => this % 3 == 0; +}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +extension E on int { + bool get isDivisibleByThree => this % 3 == 0; +}'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib2.dart'; + +void foo(int i) { + print(E(i.isDivisibleByThree)); +} +'''); + await assertHasFix( + ''' +import 'lib1.dart'; +import 'lib2.dart' hide E; + +void foo(int i) { + print(E(i.isDivisibleByThree)); +} +''', + matchFixMessage: "Hide others to use 'E' from 'lib1.dart'", + errorFilter: (error) { + return error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT; + }, + ); + } + + Future test_double_function() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +void bar() {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +void bar() {}'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib2.dart'; + +void foo() { + bar(); +} +'''); + await assertHasFix(''' +import 'lib1.dart'; +import 'lib2.dart' hide bar; + +void foo() { + bar(); +} +''', matchFixMessage: "Hide others to use 'bar' from 'lib1.dart'"); + } + + Future test_double_mixin() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +mixin M {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +mixin M {}'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib2.dart'; + +class C with M {} +'''); + await assertHasFix( + ''' +import 'lib1.dart'; +import 'lib2.dart' hide M; + +class C with M {} +''', + matchFixMessage: "Hide others to use 'M' from 'lib1.dart'", + errorFilter: (error) { + return error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT; + }, + ); + } + + Future test_double_oneHide() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class M {} class N {} class O {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' hide M, O; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' hide M, O, N; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib2.dart'"); + } + + Future test_double_oneHide_sort() async { + createAnalysisOptionsFile(lints: [LintNames.combinators_ordering]); + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class M {} class N {} class O {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' hide M, O; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' hide M, N, O; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib2.dart'"); + } + + Future test_double_oneShow() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' show N; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' show N; +import 'lib2.dart' hide N; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib1.dart'"); + } + + Future test_double_variable() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +var foo = 0;'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +var foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart' show foo; +import 'lib2.dart'; + +void f() { + print(foo); +} +'''); + await assertHasFix(''' +import 'lib1.dart' show foo; +import 'lib2.dart' hide foo; + +void f() { + print(foo); +} +''', matchFixMessage: "Hide others to use 'foo' from 'lib1.dart'"); + } + + Future test_multiLevelParts() async { + // Create a tree of files that all import 'dart:math' and ensure we find + // only the import from the parent (not a grandparent, sibling, or child). + // + // - lib1 declares A + // - lib2 declares A + // + // - root has import + // - level1_other has import + // - level1 has imports, is the used reference + // - level2_other has import + // - test has reference <-- testing this + // - level3_other has import + + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class A {} +'''); + + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class A {} +'''); + + newFile(join(testPackageLibPath, 'root.dart'), ''' +import 'lib1.dart'; +part 'level1_other.dart'; +part 'level1.dart'; +'''); + + newFile(join(testPackageLibPath, 'level1_other.dart'), ''' +part of 'root.dart'; +import 'lib1.dart'; +'''); + + newFile(join(testPackageLibPath, 'level1.dart'), ''' +part of 'root.dart'; +import 'lib1.dart'; +import 'lib2.dart'; +part 'level2_other.dart'; +part 'test.dart'; +'''); + + newFile(join(testPackageLibPath, 'level2_other.dart'), ''' +part of 'level1.dart'; +import 'lib1.dart'; +'''); + + newFile(join(testPackageLibPath, 'level3_other.dart'), ''' +part of 'test.dart'; +import 'lib1.dart'; +'''); + + await resolveTestCode(''' +part of 'level1.dart'; +part 'level3_other.dart'; + +A? a; +'''); + + await assertHasFix( + ''' +part of 'root.dart'; +import 'lib1.dart' hide A; +import 'lib2.dart'; +part 'level2_other.dart'; +part 'test.dart'; +''', + target: join(testPackageLibPath, 'level1.dart'), + matchFixMessage: "Hide others to use 'A' from 'lib2.dart'", + ); + } + + Future test_multipleCombinators() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +void bar() {} +void baz() {} +void foo() {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +void bar() {}'''); + await resolveTestCode(''' +import 'lib1.dart' hide baz hide foo; +import 'lib2.dart'; + +void foo() { + bar(); +} +'''); + await assertHasFix(''' +import 'lib1.dart' hide baz, bar hide foo, bar; +import 'lib2.dart'; + +void foo() { + bar(); +} +''', matchFixMessage: "Hide others to use 'bar' from 'lib2.dart'"); + } + + Future test_part() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'other.dart'), ''' +import 'lib1.dart'; +import 'lib2.dart'; +part 'test.dart'; +'''); + await resolveTestCode(''' +part of 'other.dart'; +import 'lib1.dart'; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +'''); + await assertHasFixesWithoutApplying( + expectedNumberOfFixesForKind: 2, + matchFixMessages: [ + "Hide others to use 'N' from 'lib1.dart'", + "Hide others to use 'N' from 'lib2.dart'", + ], + ); + await assertHasFix(''' +part of 'other.dart'; +import 'lib1.dart' hide N; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib2.dart'"); + await assertHasFix(''' +part of 'other.dart'; +import 'lib1.dart'; +import 'lib2.dart' hide N; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib1.dart'"); + } + + Future test_show_prefixed() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' as l show N; +import 'lib2.dart' as l; + +void f(l.N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' as l show N; +import 'lib2.dart' as l hide N; + +void f(l.N? n) { + print(n); +} +''', matchFixMessage: "Hide others to use 'N' from 'lib1.dart' as l"); + } + + Future test_triple() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +export 'lib3.dart';'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +mixin M {}'''); + newFile(join(testPackageLibPath, 'lib3.dart'), ''' +mixin M {}'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib2.dart'; +import 'lib3.dart'; + +class C with M {} +'''); + await assertHasFix( + ''' +import 'lib1.dart'; +import 'lib2.dart' hide M; +import 'lib3.dart' hide M; + +class C with M {} +''', + matchFixMessage: "Hide others to use 'M' from 'lib1.dart'", + errorFilter: (error) { + return error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT; + }, + ); + await assertHasFix( + ''' +import 'lib1.dart' hide M; +import 'lib2.dart'; +import 'lib3.dart' hide M; + +class C with M {} +''', + matchFixMessage: "Hide others to use 'M' from 'lib2.dart'", + errorFilter: (error) { + return error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT; + }, + ); + await assertHasFix( + ''' +import 'lib1.dart' hide M; +import 'lib2.dart' hide M; +import 'lib3.dart'; + +class C with M {} +''', + matchFixMessage: "Hide others to use 'M' from 'lib3.dart'", + errorFilter: (error) { + return error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT; + }, + ); + } + + Future test_triple_oneAliased() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +const foo = 0;'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +const foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart' as lib; +import 'lib1.dart'; +import 'lib2.dart'; + +void f() { + print(foo); +} +'''); + await assertHasFix(''' +import 'lib1.dart' as lib; +import 'lib1.dart'; +import 'lib2.dart' hide foo; + +void f() { + print(foo); +} +''', matchFixMessage: "Hide others to use 'foo' from 'lib1.dart'"); + } + + Future test_triple_twoAliased() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +const foo = 0;'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +const foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib1.dart' as lib; +import 'lib2.dart' as lib; + +void f() { + print(lib.foo); +} +'''); + await assertHasFix(''' +import 'lib1.dart'; +import 'lib1.dart' as lib; +import 'lib2.dart' as lib hide foo; + +void f() { + print(lib.foo); +} +''', matchFixMessage: "Hide others to use 'foo' from 'lib1.dart' as lib"); + } +} + +@reflectiveTest +class ImportRemoveShowTest extends FixProcessorTest { + @override + FixKind get kind => DartFixKind.IMPORT_LIBRARY_REMOVE_SHOW; + + Future test_double() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' show N; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' hide N; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Remove show to use 'N' from 'lib2.dart'"); + } + + Future test_double_aliased() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' as l show N; +import 'lib2.dart' as l show N; + +void f(l.N? n) { + print(n); +} +'''); + await assertHasFixesWithoutApplying( + expectedNumberOfFixesForKind: 2, + matchFixMessages: [ + "Remove show to use 'N' from 'lib1.dart' as l", + "Remove show to use 'N' from 'lib2.dart' as l", + ], + ); + await assertHasFix(''' +import 'lib1.dart' as l hide N; +import 'lib2.dart' as l show N; + +void f(l.N? n) { + print(n); +} +''', matchFixMessage: "Remove show to use 'N' from 'lib2.dart' as l"); + await assertHasFix(''' +import 'lib1.dart' as l show N; +import 'lib2.dart' as l hide N; + +void f(l.N? n) { + print(n); +} +''', matchFixMessage: "Remove show to use 'N' from 'lib1.dart' as l"); + } + + Future test_double_equal_importUris() async { + // https://github.com/dart-lang/sdk/issues/56830#issuecomment-2573945155 + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +var foo = 0; +var bar = 0; +var baz = 0; +'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +var foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart' show bar, foo; +import 'lib1.dart' show baz, foo; +import 'lib2.dart'; + +void f() { + print(bar); + print(baz); + print(foo); +} +'''); + await assertNoFix(); + } + + Future test_double_oneHide() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +const foo = 0;'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +const foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart' hide foo; +import 'lib2.dart' show foo; + +void f() { + print(foo); +} +'''); + await assertNoFix(); + } + + Future test_moreShow() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {} +class M {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' show N, M; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +'''); + await assertHasFixesWithoutApplying( + expectedNumberOfFixesForKind: 1, + matchFixMessages: ["Remove show to use 'N' from 'lib2.dart'"], + ); + await assertHasFix(''' +import 'lib1.dart' show M; +import 'lib2.dart'; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Remove show to use 'N' from 'lib2.dart'"); + } + + Future test_moreShow_sort() async { + createAnalysisOptionsFile(lints: [LintNames.combinators_ordering]); + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {} +class M {} +class O {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' show N, O, M; +import 'lib2.dart'; + +void f(N? n, O? o) { + print(n); +} +'''); + await assertHasFix( + ''' +import 'lib1.dart' show M, O; +import 'lib2.dart'; + +void f(N? n, O? o) { + print(n); +} +''', + errorFilter: + (error) => error.errorCode == CompileTimeErrorCode.AMBIGUOUS_IMPORT, + matchFixMessage: "Remove show to use 'N' from 'lib2.dart'", + ); + } + + Future test_multipleCombinators1() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class M {} +class N {} +class O {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' show N show N, O, M; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' show O, M; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Remove show to use 'N' from 'lib2.dart'"); + } + + Future test_multipleCombinators2() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' show N show N; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' hide N; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Remove show to use 'N' from 'lib2.dart'"); + } + + Future test_multipleCombinators3() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +class M {} +class N {}'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +class N {}'''); + await resolveTestCode(''' +import 'lib1.dart' show N hide M hide M; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +'''); + await assertHasFix(''' +import 'lib1.dart' hide M, N hide M, N; +import 'lib2.dart' show N; + +void f(N? n) { + print(n); +} +''', matchFixMessage: "Remove show to use 'N' from 'lib2.dart'"); + } + + Future test_one_show() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +var foo = 0;'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +var foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart' show foo; +import 'lib2.dart'; + +void f() { + print(foo); +} +'''); + await assertHasFix(''' +import 'lib1.dart' hide foo; +import 'lib2.dart'; + +void f() { + print(foo); +} +''', matchFixMessage: "Remove show to use 'foo' from 'lib2.dart'"); + } + + Future test_triple_twoAliased() async { + newFile(join(testPackageLibPath, 'lib1.dart'), ''' +const foo = 0;'''); + newFile(join(testPackageLibPath, 'lib2.dart'), ''' +const foo = 0;'''); + await resolveTestCode(''' +import 'lib1.dart'; +import 'lib1.dart' as lib show foo; +import 'lib2.dart' as lib; + +void f() { + print(lib.foo); +} +'''); + await assertHasFix(''' +import 'lib1.dart'; +import 'lib1.dart' as lib hide foo; +import 'lib2.dart' as lib; + +void f() { + print(lib.foo); +} +''', matchFixMessage: "Remove show to use 'foo' from 'lib2.dart' as lib"); + } +} 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 04bf16c25025..0c71576b8ef8 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 @@ -50,6 +50,7 @@ import 'add_super_parameter_test.dart' as add_super_parameter; import 'add_switch_case_break_test.dart' as add_switch_case_break; import 'add_trailing_comma_test.dart' as add_trailing_comma; import 'add_type_annotation_test.dart' as add_type_annotation; +import 'ambiguous_import_fix_test.dart' as ambiguous_import_fix; import 'analysis_options/test_all.dart' as analysis_options; import 'bulk_fix_processor_test.dart' as bulk_fix_processor; import 'change_argument_name_test.dart' as change_argument_name; @@ -427,6 +428,7 @@ void main() { fix_processor_map.main(); format_file.main(); ignore_error.main(); + ambiguous_import_fix.main(); import_library_hide.main(); import_library_prefix.main(); import_library_project.main(); diff --git a/pkg/analyzer/lib/dart/analysis/code_style_options.dart b/pkg/analyzer/lib/dart/analysis/code_style_options.dart index 09d077d0e9a8..bae080839fc3 100644 --- a/pkg/analyzer/lib/dart/analysis/code_style_options.dart +++ b/pkg/analyzer/lib/dart/analysis/code_style_options.dart @@ -28,6 +28,10 @@ abstract class CodeStyleOptions { /// The preferred quote based on the enabled lints, otherwise a single quote. String get preferredQuoteForStrings; + /// Whether combinators should be ordered alphabetically. Difined by + /// `combinators_ordering`. + bool get sortCombinators; + /// Whether constructors should be sorted first, before other class members. bool get sortConstructorsFirst; diff --git a/pkg/analyzer/lib/src/analysis_options/code_style_options.dart b/pkg/analyzer/lib/src/analysis_options/code_style_options.dart index 241bdf3cbc9e..95fecedc655c 100644 --- a/pkg/analyzer/lib/src/analysis_options/code_style_options.dart +++ b/pkg/analyzer/lib/src/analysis_options/code_style_options.dart @@ -35,6 +35,9 @@ class CodeStyleOptionsImpl implements CodeStyleOptions { @override String get preferredQuoteForStrings => _lintQuote() ?? "'"; + @override + bool get sortCombinators => _isLintEnabled('combinators_ordering'); + @override bool get sortConstructorsFirst => _isLintEnabled('sort_constructors_first');