blob: dfca1273ba84209dfda7230ed9b941f619d8d8dd [file] [log] [blame]
// Copyright (c) 2014, 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.
library pub.barback.dependency_computer;
import 'package:barback/barback.dart';
import 'package:path/path.dart' as p;
import '../dart.dart';
import '../io.dart';
import '../package.dart';
import '../package_graph.dart';
import '../utils.dart';
import 'cycle_exception.dart';
import 'transformer_config.dart';
import 'transformer_id.dart';
/// A class for determining dependencies between transformers and from Dart
/// libraries onto transformers.
class DependencyComputer {
/// The package graph being analyzed.
final PackageGraph _graph;
/// The names of packages for which [_PackageDependencyComputer]s are
/// currently loading.
///
/// This is used to detect transformer cycles. If a package's libraries or
/// transformers are referenced while the transformers that apply to it are
/// being processed, that indicates an unresolvable cycle.
final _loadingPackageComputers = new Set<String>();
/// [_PackageDependencyComputer]s that have been loaded.
final _packageComputers = new Map<String, _PackageDependencyComputer>();
/// A cache of the results of [transformersNeededByPackage].
final _transformersNeededByPackages = new Map<String, Set<TransformerId>>();
/// The set of all packages that neither use transformers themselves nor
/// import packages that use transformers.
///
/// This is precomputed before any package computers are loaded.
final _untransformedPackages = new Set<String>();
DependencyComputer(this._graph) {
for (var package in ordered(_graph.packages.keys)) {
if (_graph.transitiveDependencies(package).every((dependency) =>
dependency.pubspec.transformers.isEmpty)) {
_untransformedPackages.add(package);
}
}
ordered(_graph.packages.keys).forEach(_loadPackageComputer);
}
/// Returns a dependency graph for [transformers], or for all transformers if
/// [transformers] is `null`.
///
/// This graph is represented by a map whose keys are the vertices and whose
/// values are sets representing edges from the given vertex. Each vertex is a
/// [TransformerId]. If there's an edge from `T1` to `T2`, then `T2` must be
/// loaded before `T1` can be loaded.
///
/// The returned graph is transitively closed. That is, if there's an edge
/// from `T1` to `T2` and an edge from `T2` to `T3`, there's also an edge from
/// `T1` to `T2`.
Map<TransformerId, Set<TransformerId>> transformersNeededByTransformers(
[Iterable<TransformerId> transformers]) {
var result = {};
if (transformers == null) {
transformers = ordered(_graph.packages.keys).expand((packageName) {
var package = _graph.packages[packageName];
return package.pubspec.transformers.expand((phase) {
return phase.expand((config) {
var id = config.id;
if (id.isBuiltInTransformer) return [];
if (package.name != _graph.entrypoint.root.name &&
!config.canTransformPublicFiles) {
return [];
}
return [id];
});
});
});
}
for (var id in transformers) {
result[id] = _transformersNeededByTransformer(id);
}
return result;
}
/// Returns the set of all transformers needed to load the library identified
/// by [id].
Set<TransformerId> transformersNeededByLibrary(AssetId id) {
var library = _graph.packages[id.package].path(p.fromUri(id.path));
_loadPackageComputer(id.package);
return _packageComputers[id.package].transformersNeededByLibrary(library)
.where((id) => !id.isBuiltInTransformer).toSet();
}
/// Returns the set of all transformers that need to be loaded before [id] is
/// loaded.
Set<TransformerId> _transformersNeededByTransformer(TransformerId id) {
if (id.isBuiltInTransformer) return new Set();
_loadPackageComputer(id.package);
return _packageComputers[id.package]._transformersNeededByTransformer(id);
}
/// Returns the set of all transformers that need to be loaded before
/// [packageUri] (a "package:" URI) can be safely imported from an external
/// package.
Set<TransformerId> _transformersNeededByPackageUri(Uri packageUri) {
var components = p.split(p.fromUri(packageUri.path));
var packageName = components.first;
if (_untransformedPackages.contains(packageName)) return new Set();
var package = _graph.packages[packageName];
if (package == null) {
// TODO(nweiz): include source range information here.
fail('A transformer imported unknown package "$packageName" (in '
'"$packageUri").');
}
var library = package.path('lib', p.joinAll(components.skip(1)));
_loadPackageComputer(packageName);
return _packageComputers[packageName].transformersNeededByLibrary(library);
}
/// Returns the set of all transformers that need to be loaded before
/// everything in [rootPackage] can be used.
///
/// This is conservative in that it returns all transformers that could
/// theoretically affect [rootPackage]. It only looks at which transformers
/// packages use and which packages they depend on; it ignores imports
/// entirely.
///
/// We fall back on this conservative analysis when a transformer
/// (transitively) imports a transformed library. The result of the
/// transformation may import any dependency or hit any transformer, so we
/// have to assume that it will.
Set<TransformerId> _transformersNeededByPackage(String rootPackage) {
if (_untransformedPackages.contains(rootPackage)) return new Set();
if (_transformersNeededByPackages.containsKey(rootPackage)) {
return _transformersNeededByPackages[rootPackage];
}
var results = new Set();
var seen = new Set();
traversePackage(packageName) {
if (seen.contains(packageName)) return;
seen.add(packageName);
var package = _graph.packages[packageName];
for (var phase in package.pubspec.transformers) {
for (var config in phase) {
var id = config.id;
if (id.isBuiltInTransformer) continue;
if (_loadingPackageComputers.contains(id.package)) {
throw new CycleException("$packageName is transformed by $id");
}
results.add(id);
}
}
var dependencies = packageName == _graph.entrypoint.root.name ?
package.immediateDependencies : package.dependencies;
for (var dep in dependencies) {
try {
traversePackage(dep.name);
} on CycleException catch (error) {
throw error.prependStep("$packageName depends on ${dep.name}");
}
}
}
traversePackage(rootPackage);
_transformersNeededByPackages[rootPackage] = results;
return results;
}
/// Ensure that a [_PackageDependencyComputer] for [packageName] is loaded.
///
/// If the computer has already been loaded, this does nothing. If the
/// computer is in the process of being loaded, this throws a
/// [CycleException].
void _loadPackageComputer(String packageName) {
if (_loadingPackageComputers.contains(packageName)) {
throw new CycleException();
}
if (_packageComputers.containsKey(packageName)) return;
_loadingPackageComputers.add(packageName);
_packageComputers[packageName] =
new _PackageDependencyComputer(this, packageName);
_loadingPackageComputers.remove(packageName);
}
}
/// A helper class for [computeTransformersNeededByTransformers] that keeps
/// package-specific state and caches over the course of the computation.
class _PackageDependencyComputer {
/// The parent [DependencyComputer].
final DependencyComputer _dependencyComputer;
/// The package whose dependencies [this] is computing.
final Package _package;
/// The set of transformers that currently apply to [this].
///
/// This is added to phase-by-phase while [this] is being initialized. This is
/// necessary to model the dependencies of a transformer that's applied to its
/// own package.
final _applicableTransformers = new Set<TransformerConfig>();
/// A cache of imports and exports parsed from libraries in this package.
final _directives = new Map<Uri, Set<Uri>>();
/// The set of libraries for which there are currently active
/// [transformersNeededByLibrary] calls.
///
/// This is used to guard against infinite loops caused by libraries in
/// different packages importing one another circularly.
/// [transformersNeededByLibrary] will return an empty set for any active
/// libraries.
final _activeLibraries = new Set<String>();
/// A cache of the results of [_transformersNeededByTransformer].
final _transformersNeededByTransformers =
new Map<TransformerId, Set<TransformerId>>();
/// A cache of the results of [_getTransitiveExternalDirectives].
///
/// This is invalidated whenever [_applicableTransformers] changes.
final _transitiveExternalDirectives = new Map<String, Set<Uri>>();
_PackageDependencyComputer(DependencyComputer dependencyComputer,
String packageName)
: _dependencyComputer = dependencyComputer,
_package = dependencyComputer._graph.packages[packageName] {
// If [_package] uses its own transformers, there will be fewer transformers
// running on [_package] while its own transformers are loading than there
// will be once all its transformers are finished loading. To handle this,
// we run [_transformersNeededByTransformer] to pre-populate
// [_transformersNeededByLibraries] while [_applicableTransformers] is
// smaller.
for (var phase in _package.pubspec.transformers) {
for (var config in phase) {
var id = config.id;
try {
if (id.package != _package.name) {
// Probe [id]'s transformer dependencies to ensure that it doesn't
// depend on this package. If it does, a CycleError will be thrown.
_dependencyComputer._transformersNeededByTransformer(id);
} else {
// Store the transformers needed specifically with the current set
// of [_applicableTransformers]. When reporting this transformer's
// dependencies, [computeTransformersNeededByTransformers] will use
// this stored set of dependencies rather than the potentially wider
// set that would be recomputed if [transformersNeededByLibrary]
// were called anew.
_transformersNeededByTransformers[id] =
transformersNeededByLibrary(_package.transformerPath(id));
}
} on CycleException catch (error) {
throw error.prependStep("$packageName is transformed by $id");
}
}
// Clear the cached imports and exports because the new transformers may
// start transforming a library whose directives were previously
// statically analyzable.
_transitiveExternalDirectives.clear();
_applicableTransformers.addAll(phase);
}
}
/// Returns the set of all transformers that need to be loaded before [id] is
/// loaded.
///
/// [id] must refer to a transformer in [_package].
Set<TransformerId> _transformersNeededByTransformer(TransformerId id) {
assert(id.package == _package.name);
if (_transformersNeededByTransformers.containsKey(id)) {
return _transformersNeededByTransformers[id];
}
_transformersNeededByTransformers[id] =
transformersNeededByLibrary(_package.transformerPath(id));
return _transformersNeededByTransformers[id];
}
/// Returns the set of all transformers that need to be loaded before
/// [library] is imported.
///
/// If [library] or anything it imports/exports within this package is
/// transformed by [_applicableTransformers], this will return a conservative
/// set of transformers (see also
/// [DependencyComputer._transformersNeededByPackage]).
Set<TransformerId> transformersNeededByLibrary(String library) {
library = p.normalize(library);
if (_activeLibraries.contains(library)) return new Set();
_activeLibraries.add(library);
try {
var externalDirectives = _getTransitiveExternalDirectives(library);
if (externalDirectives == null) {
var rootName = _dependencyComputer._graph.entrypoint.root.name;
var dependencies = _package.name == rootName ?
_package.immediateDependencies : _package.dependencies;
// If anything transitively imported/exported by [library] within this
// package is modified by a transformer, we don't know what it will
// load, so we take the conservative approach and say it depends on
// everything.
return _applicableTransformers.map((config) => config.id).toSet().union(
unionAll(dependencies.map((dep) {
try {
return _dependencyComputer._transformersNeededByPackage(dep.name);
} on CycleException catch (error) {
throw error.prependStep("${_package.name} depends on ${dep.name}");
}
})));
} else {
// If nothing's transformed, then we only depend on the transformers
// used by the external packages' libraries that we import or export.
return unionAll(externalDirectives.map((uri) {
try {
return _dependencyComputer._transformersNeededByPackageUri(uri);
} on CycleException catch (error) {
var packageName = p.url.split(uri.path).first;
throw error.prependStep("${_package.name} depends on $packageName");
}
}));
}
} finally {
_activeLibraries.remove(library);
}
}
/// Returns the set of all external package libraries transitively imported or
/// exported by [rootLibrary].
///
/// All of the returned URIs will have the "package:" scheme. None of them
/// will be URIs for this package.
///
/// If [rootLibrary] transitively imports or exports a library that's modified
/// by a transformer, this will return `null`.
Set<Uri> _getTransitiveExternalDirectives(String rootLibrary) {
rootLibrary = p.normalize(rootLibrary);
if (_transitiveExternalDirectives.containsKey(rootLibrary)) {
return _transitiveExternalDirectives[rootLibrary];
}
var results = new Set();
var seen = new Set();
traverseLibrary(library) {
library = p.normalize(library);
if (seen.contains(library)) return true;
seen.add(library);
var directives = _getDirectives(library);
if (directives == null) return false;
for (var uri in directives) {
var path;
if (uri.scheme == 'package') {
var components = p.split(p.fromUri(uri.path));
if (components.first != _package.name) {
results.add(uri);
continue;
}
path = _package.path('lib', p.joinAll(components.skip(1)));
} else if (uri.scheme == '' || uri.scheme == 'file') {
path = p.join(p.dirname(library), p.fromUri(uri));
} else {
// Ignore "dart:" URIs and theoretically-possible "http:" URIs.
continue;
}
if (!traverseLibrary(path)) return false;
}
return true;
}
_transitiveExternalDirectives[rootLibrary] =
traverseLibrary(rootLibrary) ? results : null;
return _transitiveExternalDirectives[rootLibrary];
}
/// Returns the set of all imports or exports in [library].
///
/// If [library] is modified by a transformer, this will return `null`.
Set<Uri> _getDirectives(String library) {
var libraryUri = p.toUri(p.normalize(library));
var relative = p.toUri(_package.relative(library)).path;
if (_applicableTransformers.any((config) =>
config.canTransform(relative))) {
_directives[libraryUri] = null;
return null;
}
// Check the cache *after* checking [_applicableTransformers] because
// [_applicableTransformers] changes over time so the directives may be
// invalidated.
if (_directives.containsKey(libraryUri)) return _directives[libraryUri];
// If a nonexistent library is imported, it will probably be generated by a
// transformer.
if (!fileExists(library)) {
_directives[libraryUri] = null;
return null;
}
_directives[libraryUri] =
parseImportsAndExports(readTextFile(library), name: library)
.map((directive) => Uri.parse(directive.uri.stringValue))
.toSet();
return _directives[libraryUri];
}
}