blob: 00b0dd7ddc98605e2d46561502c289a6740aed32 [file] [log] [blame]
// Copyright (c) 2020, 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 'dart:collection';
import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server/src/services/correction/fix/dart/top_level_declarations.dart';
import 'package:analysis_server/src/services/correction/namespace.dart';
import 'package:analysis_server/src/services/linter/lint_names.dart';
import 'package:analysis_server/src/utilities/extensions/element.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/dart/element/type.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_dart.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';
class ImportLibrary extends MultiCorrectionProducer {
final _ImportKind _importKind;
ImportLibrary(this._importKind);
@override
Iterable<CorrectionProducer> get producers sync* {
final node = this.node;
if (_importKind == _ImportKind.dartAsync) {
yield* _importLibrary(DartFixKind.IMPORT_ASYNC, Uri.parse('dart:async'));
} else if (_importKind == _ImportKind.forExtension) {
if (node is SimpleIdentifier) {
var extensionName = node.name;
yield* _importLibraryForElement(
extensionName,
const [ElementKind.EXTENSION],
const [TopLevelDeclarationKind.extension]);
}
} else if (_importKind == _ImportKind.forExtensionMember) {
/// Return producers that will import extensions that apply to the
/// [targetType] and that define a member with the given [memberName].
Iterable<CorrectionProducer> importMatchingExtensions(
String memberName, DartType? targetType) sync* {
if (targetType == null) {
return;
}
var definingLibraries = extensionCache.membersByName[memberName];
if (definingLibraries != null) {
for (var definingLibrary in definingLibraries) {
var libraryPath = definingLibrary.libraryPath;
var uri = sessionHelper.session.uriConverter.pathToUri(libraryPath);
if (uri != null) {
yield* _importExtensionInLibrary(uri, targetType, memberName);
}
}
}
}
if (node is SimpleIdentifier) {
var memberName = node.name;
if (memberName.startsWith('_')) {
return;
}
yield* importMatchingExtensions(memberName, _targetType(node));
} else if (node is BinaryExpression) {
var memberName = node.operator.lexeme;
yield* importMatchingExtensions(
memberName, node.leftOperand.staticType);
}
} else if (_importKind == _ImportKind.forFunction) {
if (node is SimpleIdentifier) {
var parent = node.parent;
if (parent is MethodInvocation) {
if (parent.realTarget != null || parent.methodName != node) {
return;
}
}
var name = node.name;
yield* _importLibraryForElement(name, const [
ElementKind.FUNCTION,
ElementKind.TOP_LEVEL_VARIABLE
], const [
TopLevelDeclarationKind.function,
TopLevelDeclarationKind.variable
]);
}
} else if (_importKind == _ImportKind.forTopLevelVariable) {
var targetNode = node;
if (targetNode is Annotation) {
var name = targetNode.name;
if (name.staticElement == null) {
if (targetNode.arguments != null) {
return;
}
targetNode = name;
}
}
if (targetNode is SimpleIdentifier) {
var name = targetNode.name;
yield* _importLibraryForElement(
name,
const [ElementKind.TOP_LEVEL_VARIABLE],
const [TopLevelDeclarationKind.variable]);
}
} else if (_importKind == _ImportKind.forType) {
var targetNode = node;
if (targetNode is Annotation) {
var name = targetNode.name;
if (name.staticElement == null) {
if (targetNode.arguments == null) {
return;
}
targetNode = name;
}
}
if (mightBeTypeIdentifier(targetNode)) {
var typeName = (targetNode is SimpleIdentifier)
? targetNode.name
: (targetNode as PrefixedIdentifier).prefix.name;
yield* _importLibraryForElement(
typeName,
const [ElementKind.CLASS, ElementKind.FUNCTION_TYPE_ALIAS],
const [TopLevelDeclarationKind.type]);
} else if (mightBeImplicitConstructor(targetNode)) {
var typeName = (targetNode as SimpleIdentifier).name;
yield* _importLibraryForElement(typeName, const [ElementKind.CLASS],
const [TopLevelDeclarationKind.type]);
}
}
}
@override
bool mightBeTypeIdentifier(AstNode node) {
if (super.mightBeTypeIdentifier(node)) {
return true;
}
if (node is PrefixedIdentifier) {
var parent = node.parent;
if (parent is TypeName) {
return true;
}
}
return false;
}
/// Returns the relative URI from the passed [library] to the given [path].
///
/// If the [path] is not in the [library]'s directory, `null` is returned.
String? _getRelativeUriFromLibrary(LibraryElement library, String path) {
var librarySource = library.librarySource;
var pathContext = resourceProvider.pathContext;
var libraryDirectory = pathContext.dirname(librarySource.fullName);
var sourceDirectory = pathContext.dirname(path);
if (pathContext.isWithin(libraryDirectory, path) ||
pathContext.isWithin(sourceDirectory, libraryDirectory)) {
var relativeFile = pathContext.relative(path, from: libraryDirectory);
return pathContext.split(relativeFile).join('/');
}
return null;
}
Iterable<CorrectionProducer> _importExtensionInLibrary(
Uri uri, DartType targetType, String memberName) sync* {
// Look to see whether the library at the [uri] is already imported. If it
// is, then we can check the extension elements without needing to perform
// additional analysis.
var foundImport = false;
for (var imp in libraryElement.imports) {
// prepare element
var importedLibrary = imp.importedLibrary;
if (importedLibrary == null || importedLibrary.source.uri != uri) {
continue;
}
foundImport = true;
for (var extension in importedLibrary.matchingExtensionsWithMember(
libraryElement, targetType, memberName)) {
// If the import has a combinator that needs to be updated, then offer
// to update it.
var combinators = imp.combinators;
if (combinators.length == 1) {
var combinator = combinators[0];
if (combinator is HideElementCombinator) {
// TODO(brianwilkerson) Support removing the extension name from a
// hide combinator.
} else if (combinator is ShowElementCombinator) {
yield _ImportLibraryShow(
uri.toString(), combinator, extension.name!);
}
}
}
}
// If the library at the [uri] is not already imported, we return a
// correction producer that will either add an import or not based on the
// result of analyzing the library.
if (!foundImport) {
yield _ImportLibraryContainingExtension(uri, targetType, memberName);
}
}
/// Returns a list of one or two import corrections.
///
/// If [relativeUri] is `null`, only one correction, with an absolute import
/// path, is returned. Otherwise, a correction with an absolute import path
/// and a correction with a relative path are returned. If the
/// `prefer_relative_imports` lint rule is enabled, the relative path is
/// returned first.
Iterable<CorrectionProducer> _importLibrary(FixKind fixKind, Uri library,
[String? relativeUri]) {
if (relativeUri == null || relativeUri.isEmpty) {
return [_ImportAbsoluteLibrary(fixKind, library)];
}
if (isLintEnabled(LintNames.prefer_relative_imports)) {
return [
_ImportRelativeLibrary(fixKind, relativeUri),
_ImportAbsoluteLibrary(fixKind, library),
];
} else {
return [
_ImportAbsoluteLibrary(fixKind, library),
_ImportRelativeLibrary(fixKind, relativeUri),
];
}
}
Iterable<CorrectionProducer> _importLibraryForElement(
String name,
List<ElementKind> elementKinds,
List<TopLevelDeclarationKind> kinds2) sync* {
// ignore if private
if (name.startsWith('_')) {
return;
}
// may be there is an existing import,
// but it is with prefix and we don't use this prefix
var alreadyImportedWithPrefix = <String>{};
for (var imp in libraryElement.imports) {
// prepare element
var libraryElement = imp.importedLibrary;
if (libraryElement == null) {
continue;
}
var element = getExportedElement(libraryElement, name);
if (element == null) {
continue;
}
if (element is PropertyAccessorElement) {
element = element.variable;
}
if (!elementKinds.contains(element.kind)) {
continue;
}
// may be apply prefix
var prefix = imp.prefix;
if (prefix != null) {
yield _ImportLibraryPrefix(libraryElement, prefix);
continue;
}
// may be update "show" directive
var combinators = imp.combinators;
if (combinators.length == 1) {
var combinator = combinators[0];
if (combinator is HideElementCombinator) {
// TODO(brianwilkerson) Support removing the element name from a
// hide combinator.
} else if (combinator is ShowElementCombinator) {
// prepare library name - unit name or 'dart:name' for SDK library
var libraryName =
libraryElement.definingCompilationUnit.source.uri.toString();
if (libraryElement.isInSdk) {
libraryName = libraryElement.source.shortName;
}
// don't add this library again
alreadyImportedWithPrefix.add(libraryElement.source.fullName);
yield _ImportLibraryShow(libraryName, combinator, name);
}
}
}
// Find new top-level declarations.
var declarations = getTopLevelDeclarations(name);
for (var declaration in declarations) {
// Check the kind.
if (!kinds2.contains(declaration.kind)) {
continue;
}
// Check the source.
if (alreadyImportedWithPrefix.contains(declaration.path)) {
continue;
}
// Check that the import doesn't end with '.template.dart'
if (declaration.uri.path.endsWith('.template.dart')) {
continue;
}
// Compute the fix kind.
FixKind fixKind;
if (declaration.uri.isScheme('dart')) {
fixKind = DartFixKind.IMPORT_LIBRARY_SDK;
} else if (_isLibSrcPath(declaration.path)) {
// Bad: non-API.
fixKind = DartFixKind.IMPORT_LIBRARY_PROJECT3;
} else if (declaration.isExported) {
// Ugly: exports.
fixKind = DartFixKind.IMPORT_LIBRARY_PROJECT2;
} else {
// Good: direct declaration.
fixKind = DartFixKind.IMPORT_LIBRARY_PROJECT1;
}
// Add the fix.
var relativeUri =
_getRelativeUriFromLibrary(libraryElement, declaration.path);
yield* _importLibrary(fixKind, declaration.uri, relativeUri);
}
}
bool _isLibSrcPath(String path) {
var parts = resourceProvider.pathContext.split(path);
for (var i = 0; i < parts.length - 2; i++) {
if (parts[i] == 'lib' && parts[i + 1] == 'src') {
return true;
}
}
return false;
}
/// If the [node] might represent an access to a member of a type, return the
/// type of the object being accessed, otherwise return `null`.
DartType? _targetType(SimpleIdentifier node) {
var parent = node.parent;
if (parent is MethodInvocation && parent.methodName == node) {
var target = parent.realTarget;
if (target != null) {
return target.staticType;
}
} else if (parent is PropertyAccess && parent.propertyName == node) {
return parent.realTarget.staticType;
} else if (parent is PrefixedIdentifier && parent.identifier == node) {
return parent.prefix.staticType;
}
// If there is no explicit target, then return the type of an implicit
// `this`.
DartType? enclosingThisType(AstNode node) {
var parent = node.parent;
if (parent is ClassOrMixinDeclaration) {
return parent.declaredElement?.thisType;
} else if (parent is ExtensionDeclaration) {
return parent.extendedType.type;
}
}
while (parent != null) {
if (parent is MethodDeclaration) {
if (!parent.isStatic) {
return enclosingThisType(parent);
}
return null;
} else if (parent is FieldDeclaration) {
if (!parent.isStatic) {
return enclosingThisType(parent);
}
return null;
}
parent = parent.parent;
}
}
/// Return an instance of this class that will add an import of `dart:async`.
/// Used as a tear-off in `FixProcessor`.
static ImportLibrary dartAsync() => ImportLibrary(_ImportKind.dartAsync);
/// Return an instance of this class that will add an import for an extension.
/// Used as a tear-off in `FixProcessor`.
static ImportLibrary forExtension() =>
ImportLibrary(_ImportKind.forExtension);
static ImportLibrary forExtensionMember() =>
ImportLibrary(_ImportKind.forExtensionMember);
/// Return an instance of this class that will add an import for a top-level
/// function. Used as a tear-off in `FixProcessor`.
static ImportLibrary forFunction() => ImportLibrary(_ImportKind.forFunction);
/// Return an instance of this class that will add an import for a top-level
/// variable. Used as a tear-off in `FixProcessor`.
static ImportLibrary forTopLevelVariable() =>
ImportLibrary(_ImportKind.forTopLevelVariable);
/// Return an instance of this class that will add an import for a type (class
/// or mixin). Used as a tear-off in `FixProcessor`.
static ImportLibrary forType() => ImportLibrary(_ImportKind.forType);
}
/// A correction processor that can add an import using an absolute URI.
class _ImportAbsoluteLibrary extends CorrectionProducer {
final FixKind _fixKind;
final Uri _library;
String _uriText = '';
_ImportAbsoluteLibrary(this._fixKind, this._library);
@override
List<Object> get fixArguments => [_uriText];
@override
FixKind get fixKind => _fixKind;
@override
Future<void> compute(ChangeBuilder builder) async {
await builder.addDartFileEdit(file, (builder) {
_uriText = builder.importLibrary(_library);
});
}
}
enum _ImportKind {
dartAsync,
forExtension,
forExtensionMember,
forFunction,
forTopLevelVariable,
forType
}
/// A correction processor that can add an import of a library containing an
/// extension, but which does so only if the extension applies to a given type.
class _ImportLibraryContainingExtension extends CorrectionProducer {
/// The URI of the library defining the extension.
Uri uri;
/// The type of the target that the extension must apply to.
DartType targetType;
/// The name of the member that the extension must declare.
String memberName;
/// The URI that is being proposed for the import directive.
String _uriText = '';
_ImportLibraryContainingExtension(this.uri, this.targetType, this.memberName);
@override
List<Object> get fixArguments => [_uriText];
@override
FixKind get fixKind => DartFixKind.IMPORT_LIBRARY_PROJECT1;
@override
Future<void> compute(ChangeBuilder builder) async {
var result = await sessionHelper.session.getLibraryByUri(uri.toString());
if (result is LibraryElementResult) {
var library = result.element;
if (library
.matchingExtensionsWithMember(libraryElement, targetType, memberName)
.isNotEmpty) {
await builder.addDartFileEdit(file, (builder) {
_uriText = builder.importLibrary(uri);
});
}
}
}
}
/// A correction processor that can add a prefix to an identifier defined in a
/// library that is already imported but that is imported with a prefix.
class _ImportLibraryPrefix extends CorrectionProducer {
final LibraryElement _importedLibrary;
final PrefixElement _importPrefix;
String _libraryName = '';
String _prefixName = '';
_ImportLibraryPrefix(this._importedLibrary, this._importPrefix);
@override
List<Object> get fixArguments => [_libraryName, _prefixName];
@override
FixKind get fixKind => DartFixKind.IMPORT_LIBRARY_PREFIX;
@override
Future<void> compute(ChangeBuilder builder) async {
_libraryName = _importedLibrary.displayName;
_prefixName = _importPrefix.displayName;
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(range.startLength(node, 0), '$_prefixName.');
});
}
}
/// A correction processor that can add a name to the show combinator of an
/// existing import.
class _ImportLibraryShow extends CorrectionProducer {
final String _libraryName;
final ShowElementCombinator _showCombinator;
final String _addedName;
_ImportLibraryShow(this._libraryName, this._showCombinator, this._addedName);
@override
List<Object> get fixArguments => [_libraryName];
@override
FixKind get fixKind => DartFixKind.IMPORT_LIBRARY_SHOW;
@override
Future<void> compute(ChangeBuilder builder) async {
Set<String> showNames = SplayTreeSet<String>();
showNames.addAll(_showCombinator.shownNames);
showNames.add(_addedName);
var newShowCode = 'show ${showNames.join(', ')}';
var offset = _showCombinator.offset;
var length = _showCombinator.end - offset;
var libraryFile = resolvedResult.libraryElement.source.fullName;
await builder.addDartFileEdit(libraryFile, (builder) {
builder.addSimpleReplacement(SourceRange(offset, length), newShowCode);
});
}
}
/// A correction processor that can add an import using a relative URI.
class _ImportRelativeLibrary extends CorrectionProducer {
final FixKind _fixKind;
final String _relativeURI;
_ImportRelativeLibrary(this._fixKind, this._relativeURI);
@override
List<Object> get fixArguments => [_relativeURI];
@override
FixKind get fixKind => _fixKind;
@override
Future<void> compute(ChangeBuilder builder) async {
await builder.addDartFileEdit(file, (builder) {
if (builder is DartFileEditBuilderImpl) {
builder.importLibraryWithRelativeUri(_relativeURI);
}
});
}
}