Skip to content

New assist to add/edit hide at import for ambiguous import #56762

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
00b4627
new assist to add/edit `hide` at import for ambiguous import
FMorschel Sep 20, 2024
4ce536a
sorting file
FMorschel Oct 3, 2024
4848ad2
refactor to simplify user choice and automation
FMorschel Oct 10, 2024
9e02a21
readding the ambiguous import fix
FMorschel Oct 16, 2024
42786c5
better handling of different prefixes same imported library
FMorschel Oct 22, 2024
5c8a793
makes two fixes - considers discussion
FMorschel Nov 1, 2024
1bd706c
migrates to new element model and fixes tests
FMorschel Nov 14, 2024
94052ab
new assist to add/edit `hide` at import for ambiguous import
FMorschel Sep 20, 2024
b957f39
sorting file
FMorschel Oct 3, 2024
ce153c4
refactor to simplify user choice and automation
FMorschel Oct 10, 2024
3a3ccdb
readding the ambiguous import fix
FMorschel Oct 16, 2024
5f8be78
better handling of different prefixes same imported library
FMorschel Oct 22, 2024
15b07f1
makes two fixes - considers discussion
FMorschel Nov 1, 2024
1767e1a
refactors - review
FMorschel Nov 14, 2024
0afc8e4
sorts test file
FMorschel Nov 14, 2024
4a6b92a
refactors to simplify the producers calculation - review
FMorschel Nov 14, 2024
7de35d0
adds support for part files fixing
FMorschel Nov 19, 2024
86f855d
new multi level part tests
FMorschel Nov 22, 2024
3d1b9fa
expected result for failing test
FMorschel Nov 22, 2024
9fe08d0
fixes previously failing test
FMorschel Nov 22, 2024
b7052c8
readded removed (by mistake) lines on merge
FMorschel Dec 26, 2024
df4fb70
removes trailing white space
FMorschel Dec 26, 2024
4566cce
rename files
FMorschel Dec 26, 2024
8b842c7
docs and sorting of combinators considering lint
FMorschel Dec 26, 2024
f71887a
sorting and formatting
FMorschel Dec 26, 2024
6f7af3b
refactors - reviews
FMorschel Jan 5, 2025
da06362
changes set->list and sorts files
FMorschel Jan 6, 2025
08157b0
removes fix when multiple equal uri
FMorschel Jan 6, 2025
3c365c6
formatts files and changes the copyright year
FMorschel Jan 16, 2025
02cf2e6
removes todo for unexisting utility change
FMorschel Jan 16, 2025
45812df
fixes multiple combinators
FMorschel Jan 18, 2025
19a0595
fixes for multiple hide
FMorschel Jan 21, 2025
24e3507
resolves missing fixkinds
FMorschel Jan 30, 2025
0c4ba32
fixes broken tests and formats files
FMorschel Jan 30, 2025
3f489e2
readds missing fix from rebase
FMorschel Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<List<ResolvedCorrectionProducer>> 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 = <ResolvedCorrectionProducer>[];
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<ImportDirective>, List<String>)
_getImportDirectives(
ResolvedLibraryResult libraryResult,
ResolvedUnitResult? unitResult,
List<Element2> conflictingElements,
String name,
String? prefix,
) {
// The uris of all import directives that import the conflicting elements.
var uris = <String>[];
// The import directives that import the conflicting elements.
var importDirectives = <ImportDirective>[];

// 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<ImportDirective>()) {
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<ImportDirective> 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<String> 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<void> compute(ChangeBuilder builder) async {
if (_elementName.isEmpty || uri.isEmpty) {
return;
}

var hideCombinators =
<({ImportDirective directive, List<HideCombinator> hideList})>[];

for (var directive in importDirectives) {
var show = directive.combinators.whereType<ShowCombinator>().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<HideCombinator>().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<ImportDirective> 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<String> 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<void> compute(ChangeBuilder builder) async {
if (_elementName.isEmpty || uri.isEmpty) {
return;
}

var showCombinators =
<
({
ImportDirective directive,
List<ShowCombinator> showList,
List<HideCombinator> hideList,
})
>[];

for (var directive in importDirectives) {
var show = directive.combinators.whereType<ShowCombinator>().toList();
var hide = directive.combinators.whereType<HideCombinator>().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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: |-
Expand Down
20 changes: 15 additions & 5 deletions pkg/analysis_server/lib/src/services/correction/fix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading