blob: 4188a886b8d0da2958f1986c3a34882bf3df3c86 [file] [log] [blame]
// Copyright (c) 2022, 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.md file.
import 'package:_fe_analyzer_shared/src/util/link.dart';
import 'package:_fe_analyzer_shared/src/util/relativize.dart' as uri_extras;
import 'builder/library_builder.dart';
/// Compute the set of distinct import chains to the library at [uri] within
/// [loadedLibraries].
///
/// The chains are strings of the form
///
/// <main-uri> => <intermediate-uri1> => <intermediate-uri2> => <uri>
///
Set<String> computeImportChainsFor(
Uri entryPoint, LoadedLibraries loadedLibraries, Uri uri,
{required bool verbose}) {
// TODO(johnniwinther): Move computation of dependencies to the library
// loader.
Set<String> importChains = new Set<String>();
// The maximum number of full imports chains to process.
final int chainLimit = 10000;
// The maximum number of imports chains to show.
final int compactChainLimit = verbose ? 20 : 10;
int chainCount = 0;
loadedLibraries.forEachImportChain(uri,
callback: (Link<Uri> importChainReversed) {
// The import chain is provided in reverse order, from the target to the
// entry point. To reverse it, we create a new chain, prepending the uris
// of the reversed chain.
Link<CodeLocation> compactImportChain = const Link<CodeLocation>();
CodeLocation currentCodeLocation =
new UriLocation(importChainReversed.head);
compactImportChain = compactImportChain.prepend(currentCodeLocation);
for (Link<Uri> link = importChainReversed.tail!;
!link.isEmpty;
link = link.tail!) {
Uri uri = link.head;
if (!currentCodeLocation.inSameLocation(uri)) {
// When [verbose] is `false` we use the [CodeLocation] of the [uri]
// rather than the [UriLocation], which means that we group all
// libraries with the same package into one location, thereby shortening
// the path from, for instance,
//
// package:foo/foo1.dart => package:foo/foo2.dart =>
// package:bar/bar1.dart => package:bar/bar2.dart =>
// package:baz/baz1.dart => package:baz/baz2.dart
//
// to
//
// package:foo => package:bar => package:baz
//
currentCodeLocation =
verbose ? new UriLocation(uri) : new CodeLocation(uri);
compactImportChain = compactImportChain.prepend(currentCodeLocation);
}
}
String importChain = compactImportChain.map((CodeLocation codeLocation) {
return codeLocation.relativize(entryPoint);
}).join(' => ');
if (!importChains.contains(importChain)) {
if (importChains.length > compactChainLimit) {
importChains.add('...');
return false;
} else {
importChains.add(importChain);
}
}
chainCount++;
if (chainCount > chainLimit) {
// Assume there are more import chains.
importChains.add('...');
return false;
}
return true;
});
return importChains;
}
/// Interface for computing import chains on a set of libraries.
abstract class LoadedLibraries {
/// Applies all imports chains of [uri] with the set of libraries to
/// [callback].
///
/// The argument [importChainReversed] to [callback] contains the chain of
/// imports uris that lead to importing [uri] starting in [uri] and ending in
/// the entry point uri.
///
/// [callback] is called once for each chain of imports leading to [uri] until
/// [callback] returns `false`.
void forEachImportChain(Uri uri,
{required bool callback(Link<Uri> importChainReversed)});
}
class LoadedLibrariesImpl implements LoadedLibraries {
/// The library of the compilation entry point.
final LibraryBuilder rootLibrary;
final Map<Uri, LibraryBuilder> libraryBuilders = <Uri, LibraryBuilder>{};
// TODO(johnniwinther): Support multiple entry-points.
LoadedLibrariesImpl(this.rootLibrary, Iterable<LibraryBuilder> libraries) {
libraries.forEach((LibraryBuilder libraryBuilder) {
libraryBuilders[libraryBuilder.importUri] = libraryBuilder;
});
}
@override
void forEachImportChain(Uri targetUri,
{required bool callback(Link<Uri> importChainReversed)}) {
bool aborted = false;
/// Map from libraries to the set of (unreversed) paths to [targetUri].
Map<LibraryBuilder, Iterable<Link<Uri>>> suffixChainMap =
<LibraryBuilder, Iterable<Link<Uri>>>{};
/// Computes the set of (unreversed) paths to [targetUri].
///
/// Finds all paths (suffixes) from the current [library] to [targetUri] and
/// stores it in [suffixChainMap].
///
/// For every found suffix it prepends the given [prefix] and the [library]
/// and invokes the [callback] with the concatenated chain.
void computeSuffixes(LibraryBuilder library, Link<Uri> prefix) {
if (aborted) return;
Uri canonicalUri = library.importUri;
prefix = prefix.prepend(canonicalUri);
suffixChainMap[library] = const <Link<Uri>>[];
List<Link<Uri>> suffixes = [];
if (targetUri != canonicalUri) {
/// Process the import (or export) of [importedLibrary].
void processLibrary(LibraryBuilder importedLibrary) {
bool suffixesArePrecomputed =
suffixChainMap.containsKey(importedLibrary);
if (!suffixesArePrecomputed) {
computeSuffixes(importedLibrary, prefix);
if (aborted) return;
}
for (Link<Uri> suffix in suffixChainMap[importedLibrary]!) {
suffixes.add(suffix.prepend(canonicalUri));
if (suffixesArePrecomputed) {
// Only report chains through [importedLibrary] if the suffixes
// had already been computed, otherwise [computeSuffixes] have
// reported the paths through [prefix].
Link<Uri> chain = prefix.reversePrependAll(suffix);
if (!callback(chain)) {
aborted = true;
return;
}
}
}
}
for (Uri dependency in library.dependencies) {
LibraryBuilder? libraryBuilder = libraryBuilders[dependency];
if (libraryBuilder != null) {
// Library builder is only available if the dependency has been
// loaded.
processLibrary(libraryBuilder);
}
if (aborted) return;
}
} else {
// Here `targetUri == canonicalUri`.
if (!callback(prefix)) {
aborted = true;
return;
}
suffixes.add(const Link<Uri>().prepend(canonicalUri));
}
suffixChainMap[library] = suffixes;
return;
}
computeSuffixes(rootLibrary, const Link<Uri>());
}
@override
String toString() => 'root=$rootLibrary,libraries=${libraryBuilders.keys}';
}
/// [CodeLocation] divides uris into different classes.
///
/// These are used to group uris from user code, platform libraries and
/// packages.
abstract class CodeLocation {
/// Returns `true` if [uri] is in this code location.
bool inSameLocation(Uri uri);
/// Returns the uri of this location relative to [baseUri].
String relativize(Uri baseUri);
factory CodeLocation(Uri uri) {
if (uri.isScheme('package')) {
int slashPos = uri.path.indexOf('/');
if (slashPos != -1) {
String packageName = uri.path.substring(0, slashPos);
return new PackageLocation(packageName);
} else {
// This is an invalid import uri, like "package:foo.dart".
return new UriLocation(uri);
}
} else {
return new SchemeLocation(uri);
}
}
}
/// A code location defined by the scheme of the uri.
///
/// Used for non-package uris, such as 'dart', 'file', and 'http'.
class SchemeLocation implements CodeLocation {
final Uri uri;
SchemeLocation(this.uri);
@override
bool inSameLocation(Uri uri) {
return this.uri.scheme == uri.scheme;
}
@override
String relativize(Uri baseUri) {
return uri_extras.relativizeUri(baseUri, uri, false);
}
}
/// A code location defined by the package name.
///
/// Used for package uris, separated by their `package names`, that is, the
/// 'foo' of 'package:foo/bar.dart'.
class PackageLocation implements CodeLocation {
final String packageName;
PackageLocation(this.packageName);
@override
bool inSameLocation(Uri uri) {
return uri.scheme == 'package' && uri.path.startsWith('$packageName/');
}
@override
String relativize(Uri baseUri) => 'package:$packageName';
}
/// A code location defined by the whole uri.
///
/// Used for package uris with no package name. For instance 'package:foo.dart'.
class UriLocation implements CodeLocation {
final Uri uri;
UriLocation(this.uri);
@override
bool inSameLocation(Uri uri) => this.uri == uri;
@override
String relativize(Uri baseUri) {
return uri_extras.relativizeUri(baseUri, uri, false);
}
}