diff --git a/lib/src/barback.dart b/lib/src/barback.dart index 4467531a58a28a89ae1c5ef926bcb3dd9eea9189..111b7689257095c526c9cc136a97891735915a74 100644 --- a/lib/src/barback.dart +++ b/lib/src/barback.dart @@ -7,8 +7,10 @@ library pub.barback; import 'dart:async'; import 'package:barback/barback.dart'; -import 'package:path/path.dart' as path; +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; +import 'io.dart'; import 'utils.dart'; import 'version.dart'; @@ -173,13 +175,13 @@ class TransformerId { throw new FormatException('Unsupported built-in transformer $package.'); } - // TODO(nweiz): support deep equality on [configuration] as well. bool operator==(other) => other is TransformerId && other.package == package && other.path == path && - other.configuration == configuration; + const DeepCollectionEquality().equals(other.configuration, configuration); - int get hashCode => package.hashCode ^ path.hashCode ^ configuration.hashCode; + int get hashCode => package.hashCode ^ path.hashCode ^ + const DeepCollectionEquality().hash(configuration); String toString() => path == null ? package : '$package/$path'; @@ -197,11 +199,26 @@ class TransformerId { test: (e) => e is AssetNotFoundException); } + /// Returns the path to the library identified by this transformer within + /// [packageDir], which should be the directory of [package]. + /// + /// If `path` is null, this will determine which library to load. Unlike + /// [getAssetId], this doesn't take generated assets into account; it's used + /// to determine transformers' dependencies, which requires looking at files + /// on disk. + String getFullPath(String packageDir) { + if (path != null) return p.join(packageDir, 'lib', p.fromUri('$path.dart')); + + var transformerPath = p.join(packageDir, 'lib', 'transformer.dart'); + if (fileExists(transformerPath)) return transformerPath; + return p.join(packageDir, 'lib', '$package.dart'); + } + /// Returns whether the include/exclude rules allow the transformer to run on /// [pathWithinPackage]. /// - /// [pathWithinPackage] must be a path relative to the containing package's - /// root directory. + /// [pathWithinPackage] must be a URL-style path relative to the containing + /// package's root directory. bool canTransform(String pathWithinPackage) { // TODO(rnystrom): Support globs in addition to paths. See #17093. if (excludes != null) { @@ -224,7 +241,7 @@ Uri idToPackageUri(AssetId id) { } return new Uri(scheme: 'package', - path: path.url.join(id.package, id.path.replaceFirst('lib/', ''))); + path: p.url.join(id.package, id.path.replaceFirst('lib/', ''))); } /// Converts [uri] into an [AssetId] if its path is within "packages". @@ -234,7 +251,7 @@ Uri idToPackageUri(AssetId id) { /// /// If the URI doesn't contain one of those special directories, returns null. AssetId packagesUrlToId(Uri url) { - var parts = path.url.split(url.path); + var parts = p.url.split(url.path); // Strip the leading "/" from the URL. if (parts.isNotEmpty && parts.first == "/") parts = parts.skip(1).toList(); @@ -256,6 +273,6 @@ AssetId packagesUrlToId(Uri url) { } var package = parts[index + 1]; - var assetPath = path.url.join("lib", path.url.joinAll(parts.skip(index + 2))); + var assetPath = p.url.join("lib", p.url.joinAll(parts.skip(index + 2))); return new AssetId(package, assetPath); } diff --git a/lib/src/barback/asset_environment.dart b/lib/src/barback/asset_environment.dart index 71dd4decde22a6bd1d21714c93b3e0d729f58e87..0c5a0340388d054d1011704ff36bc79e31cbaf06 100644 --- a/lib/src/barback/asset_environment.dart +++ b/lib/src/barback/asset_environment.dart @@ -9,7 +9,6 @@ import 'dart:io'; import 'package:barback/barback.dart'; import 'package:path/path.dart' as path; -import 'package:stack_trace/stack_trace.dart'; import 'package:watcher/watcher.dart'; import '../entrypoint.dart'; diff --git a/lib/src/barback/cycle_exception.dart b/lib/src/barback/cycle_exception.dart new file mode 100644 index 0000000000000000000000000000000000000000..c417ecf27ef526c238b6fd8a313941094ebfb9ab --- /dev/null +++ b/lib/src/barback/cycle_exception.dart @@ -0,0 +1,65 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS d.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.cycle_exception; + +import 'package:stack_trace/stack_trace.dart'; + +import '../utils.dart'; + +/// An exception thrown when a transformer dependency cycle is detected. +/// +/// A cycle exception is usually produced within a deeply-nested series of +/// calls. The API is designed to make it easy for each of these calls to add to +/// the message so that the full reasoning for the cycle is made visible to the +/// user. +/// +/// Each call's individual message is called a "step". A [CycleException] is +/// represented internally as a linked list of steps. +class CycleException implements ApplicationException { + final innerError = null; + final Trace innerTrace = null; + + /// The step for this exception. + final String _step; + + /// The next exception in the linked list. + /// + /// [_next]'s steps come after [_step]. + final CycleException _next; + + /// A list of all steps in the cycle. + List<String> get steps { + if (_step == null) return []; + + var exception = this; + var steps = []; + while (exception != null) { + steps.add(exception._step); + exception = exception._next; + } + return steps; + } + + String get message { + var steps = this.steps; + if (steps.isEmpty) return "Transformer cycle detected."; + return "Transformer cycle detected:\n" + + steps.map((step) => " $step").join("\n"); + } + + /// Creates a new [CycleException] with zero or one steps. + CycleException([this._step]) + : _next = null; + + CycleException._(this._step, this._next); + + /// Returns a copy of [this] with [step] added to the beginning of [steps]. + CycleException prependStep(String step) { + if (_step == null) return new CycleException(step); + return new CycleException._(step, this); + } + + String toString() => message; +} diff --git a/lib/src/barback/load_all_transformers.dart b/lib/src/barback/load_all_transformers.dart index f8827f2ce79fe57b0101572ab6755f890cff4e93..bc14ea5a70e25316c08d064a4591b88edae2cf4d 100644 --- a/lib/src/barback/load_all_transformers.dart +++ b/lib/src/barback/load_all_transformers.dart @@ -13,11 +13,12 @@ import '../log.dart' as log; import '../package_graph.dart'; import '../utils.dart'; import 'asset_environment.dart'; +import 'barback_server.dart'; import 'dart2js_transformer.dart'; import 'excluding_transformer.dart'; import 'load_transformers.dart'; import 'rewrite_import_transformer.dart'; -import 'barback_server.dart'; +import 'transformers_needed_by_transformers.dart'; /// Loads all transformers depended on by packages in [environment]. /// @@ -29,33 +30,26 @@ import 'barback_server.dart'; /// automatically be added to the end of the root package's cascade. Future loadAllTransformers(AssetEnvironment environment, BarbackServer transformerServer) { - // In order to determine in what order we should load transformers, we need to - // know which transformers depend on which others. This is different than - // normal package dependencies. Let's begin with some terminology: - // - // * If package A is transformed by package B, we say A has a "transformer - // dependency" on B. - // * If A imports B we say A has a "package dependency" on B. - // * If A needs B's transformers to be loaded in order to load A's - // transformers, we say A has an "ordering dependency" on B. - // - // In particular, an ordering dependency is defined as follows: - // - // * If A has a transformer dependency on B, A also has an ordering dependency - // on B. - // * If A has a transitive package dependency on B and B has a transformer - // dependency on C, A has an ordering dependency on C. - // - // The order that transformers are loaded is determined by each package's - // ordering dependencies. We treat the packages as a directed acyclic[1] graph - // where each package is a node and the ordering dependencies are the edges - // (that is, the packages form a partially ordered set). We then load[2] - // packages in a topological sort order of this graph. - // - // [1] TODO(nweiz): support cycles in some cases. - // - // [2] We use "loading a package" as a shorthand for loading that package's - // transformers. + var transformersNeededByTransformers = + computeTransformersNeededByTransformers(environment.graph); + + var buffer = new StringBuffer(); + buffer.writeln("Transformer dependencies:"); + transformersNeededByTransformers.forEach((id, dependencies) { + if (dependencies.isEmpty) { + buffer.writeln("$id: -"); + } else { + buffer.writeln("$id: ${toSentence(dependencies)}"); + } + }); + log.fine(buffer); + + var phasedTransformers = _phaseTransformers(transformersNeededByTransformers); + + var packagesThatUseTransformers = + _packagesThatUseTransformers(environment.graph); + + var loader = new _TransformerLoader(environment, transformerServer); // Add a rewrite transformer for each package, so that we can resolve // "package:" imports while loading transformers. @@ -65,67 +59,26 @@ Future loadAllTransformers(AssetEnvironment environment, } environment.barback.updateTransformers(r'$pub', [[rewrite]]); - var orderingDeps = _computeOrderingDeps(environment.graph); - var reverseOrderingDeps = reverseGraph(orderingDeps); - var packageTransformers = _computePackageTransformers(environment.graph); - - var loader = new _TransformerLoader(environment, transformerServer); - - // The packages on which no packages have ordering dependencies -- that is, - // the packages that don't need to be loaded before any other packages. These - // packages will be loaded last, since all of their ordering dependencies need - // to be loaded before they're loaded. However, they'll be traversed by - // [loadPackage] first. - var rootPackages = environment.graph.packages.keys.toSet() - .difference(unionAll(orderingDeps.values)); - - // The Futures for packages that have been loaded or are being actively loaded - // by [loadPackage]. Once one of these Futures is complete, the transformers - // for that package will all be available from [loader]. - var loadingPackages = new Map<String, Future>(); - - // A helper function that loads all the transformers that [package] uses, then - // all the transformers that [package] defines. - Future loadPackage(String package) { - if (loadingPackages.containsKey(package)) return loadingPackages[package]; - - // First, load each package upon which [package] has an ordering dependency. - var future = Future.wait(orderingDeps[package].map(loadPackage)).then((_) { - // Go through the transformers used by [package] phase-by-phase. If any - // phase uses a transformer defined in [package] itself, that transformer - // should be loaded after running all previous phases. - var transformers = [[rewrite]]; - - var phases = environment.graph.packages[package].pubspec.transformers; - return Future.forEach(phases, (phase) { - return loader.load(phase.where((id) => id.package == package)) - .then((_) { - // If we've already loaded all the transformers in this package and no - // other package imports it, there's no need to keep applying - // transformers, so we can short-circuit. - var loadedAllTransformers = packageTransformers[package] - .difference(loader.loadedTransformers).isEmpty; - if (loadedAllTransformers && - !reverseOrderingDeps.containsKey(package)) { - return null; - } - - transformers.add(unionAll(phase.map( - (id) => loader.transformersFor(id)))); - environment.barback.updateTransformers(package, transformers); - }); - }).then((_) { - // Now that we've applied all the transformers used by [package] via - // [Barback.updateTransformers], we load any transformers defined in - // [package] but used elsewhere. - return loader.load(packageTransformers[package]); - }); + return Future.forEach(phasedTransformers, (phase) { + /// Load all the transformers in [phase], then add them to the appropriate + /// locations in the transformer graphs of the packages that use them. + return loader.load(phase).then((_) { + // Only update packages that use transformers in [phase]. + var packagesToUpdate = unionAll(phase.map((id) => + packagesThatUseTransformers[id])); + for (var packageName in packagesToUpdate) { + var package = environment.graph.packages[packageName]; + var transformers = package.pubspec.transformers.map((packagePhase) { + return unionAll(packagePhase.map(loader.transformersFor)); + }).toList(); + + // Make sure [rewrite] is still the first phase so that future + // transformers' "package:" imports will work. + transformers.insert(0, [rewrite]); + environment.barback.updateTransformers(packageName, transformers); + } }); - loadingPackages[package] = future; - return future; - } - - return Future.wait(rootPackages.map(loadPackage)).then((_) { + }).then((_) { /// Reset the transformers for each package to get rid of [rewrite], which /// is no longer needed. for (var package in environment.graph.packages.values) { @@ -148,85 +101,50 @@ Future loadAllTransformers(AssetEnvironment environment, }); } -/// Computes and returns the graph of ordering dependencies for [graph]. +/// Given [transformerDependencies], a directed acyclic graph, returns a list of +/// "phases" (sets of transformers). /// -/// This graph is in the form of a map whose keys are packages and whose values -/// are those packages' ordering dependencies. -Map<String, Set<String>> _computeOrderingDeps(PackageGraph graph) { - var orderingDeps = new Map<String, Set<String>>(); - // Iterate through the packages in a deterministic order so that if there are - // multiple cycles we choose which to print consistently. - var packages = ordered(graph.packages.values.map((package) => package.name)); - for (var package in packages) { - // This package's transformer dependencies are also ordering dependencies. - var deps = _transformerDeps(graph, package); - deps.remove(package); - // The transformer dependencies of this package's transitive package - // dependencies are also ordering dependencies for this package. - var transitivePackageDeps = graph.transitiveDependencies(package) - .map((package) => package.name); - for (var packageDep in ordered(transitivePackageDeps)) { - var transformerDeps = _transformerDeps(graph, packageDep); - if (transformerDeps.contains(package)) { - throw _cycleError(graph, package, packageDep); - } - deps.addAll(transformerDeps); - } - orderingDeps[package] = deps; +/// Each phase must be fully loaded and passed to barback before the next phase +/// can be safely loaded. However, transformers within a phase can be safely +/// loaded in parallel. +List<Set<TransformerId>> _phaseTransformers( + Map<TransformerId, Set<TransformerId>> transformerDependencies) { + // A map from transformer ids to the indices of the phases that those + // transformer ids should end up in. Populated by [phaseNumberFor]. + var phaseNumbers = {}; + var phases = []; + + phaseNumberFor(id) { + if (phaseNumbers.containsKey(id)) return phaseNumbers[id]; + var dependencies = transformerDependencies[id]; + phaseNumbers[id] = dependencies.isEmpty ? 0 : + maxAll(dependencies.map(phaseNumberFor)) + 1; + return phaseNumbers[id]; } - return orderingDeps; -} - -/// Returns the set of transformer dependencies for [package]. -Set<String> _transformerDeps(PackageGraph graph, String package) => - unionAll(graph.packages[package].pubspec.transformers) - .where((id) => !id.isBuiltInTransformer) - .map((id) => id.package).toSet(); + for (var id in transformerDependencies.keys) { + var phaseNumber = phaseNumberFor(id); + if (phases.length <= phaseNumber) phases.length = phaseNumber + 1; + if (phases[phaseNumber] == null) phases[phaseNumber] = new Set(); + phases[phaseNumber].add(id); + } -/// Returns an [ApplicationException] describing an ordering dependency cycle -/// detected in [graph]. -/// -/// [dependee] and [depender] should be the names of two packages known to be in -/// the cycle. In addition, [depender] should have a transformer dependency on -/// [dependee]. -ApplicationException _cycleError(PackageGraph graph, String dependee, - String depender) { - assert(_transformerDeps(graph, depender).contains(dependee)); - - var simpleGraph = mapMap(graph.packages, value: (_, package) => - package.dependencies.map((dep) => dep.name).toList()); - var path = shortestPath(simpleGraph, dependee, depender); - path.add(dependee); - return new ApplicationException("Transformer cycle detected:\n" + - pairs(path).map((pair) { - var transformers = unionAll(graph.packages[pair.first].pubspec.transformers) - .where((id) => id.package == pair.last) - .map((id) => id.toString()).toList(); - if (transformers.isEmpty) { - return " ${pair.first} depends on ${pair.last}"; - } else { - return " ${pair.first} is transformed by ${toSentence(transformers)}"; - } - }).join("\n")); + return phases; } -/// Returns a map from each package name in [graph] to the transformer ids of -/// all transformers exposed by that package and used by other packages. -Map<String, Set<TransformerId>> _computePackageTransformers( +/// Returns a map from transformer ids to all packages in [graph] that use each +/// transformer. +Map<TransformerId, Set<String>> _packagesThatUseTransformers( PackageGraph graph) { - var packageTransformers = new Map.fromIterable(graph.packages.values, - key: (package) => package.name, - value: (_) => new Set<TransformerId>()); + var results = {}; for (var package in graph.packages.values) { for (var phase in package.pubspec.transformers) { for (var id in phase) { - if (id.isBuiltInTransformer) continue; - packageTransformers[id.package].add(id); + results.putIfAbsent(id, () => new Set()).add(package.name); } } } - return packageTransformers; + return results; } /// A class that loads transformers defined in specific files. @@ -304,12 +222,12 @@ class _TransformerLoader { /// Returns the set of transformers for [id]. /// - /// It's an error to call this before [load] is called with [id] and the - /// future it returns has completed. + /// If this is called before [load] for a given [id], it will return an empty + /// set. Set<Transformer> transformersFor(TransformerId id) { if (_transformers.containsKey(id)) return _transformers[id]; + if (id.package != '\$dart2js') return new Set(); - assert(id.package == '\$dart2js'); var transformer; try { transformer = new Dart2JSTransformer.withSettings(_environment, @@ -324,4 +242,4 @@ class _TransformerLoader { _transformers[id] = new Set.from([transformer]); return _transformers[id]; } -} \ No newline at end of file +} diff --git a/lib/src/barback/rewrite_import_transformer.dart b/lib/src/barback/rewrite_import_transformer.dart index 8392289e3e33d8d0d4d7b8815b84a6f36b7ace63..7482d620a840df40be7a5ecdb6c093888f231a6a 100644 --- a/lib/src/barback/rewrite_import_transformer.dart +++ b/lib/src/barback/rewrite_import_transformer.dart @@ -7,7 +7,8 @@ library pub.rewrite_import_transformer; import 'dart:async'; import 'package:barback/barback.dart'; -import 'package:analyzer/analyzer.dart'; + +import '../dart.dart'; /// A transformer used internally to rewrite "package:" imports so they point to /// the barback server rather than to pub's package root. @@ -16,13 +17,12 @@ class RewriteImportTransformer extends Transformer { Future apply(Transform transform) { return transform.primaryInput.readAsString().then((contents) { - var collector = new _DirectiveCollector(); - parseDirectives(contents, name: transform.primaryInput.id.toString()) - .accept(collector); + var directives = parseImportsAndExports(contents, + name: transform.primaryInput.id.toString()); var buffer = new StringBuffer(); var index = 0; - for (var directive in collector.directives) { + for (var directive in directives) { var uri = Uri.parse(directive.uri.stringValue); if (uri.scheme != 'package') continue; @@ -38,10 +38,3 @@ class RewriteImportTransformer extends Transformer { }); } } - -/// A simple visitor that collects import and export nodes. -class _DirectiveCollector extends GeneralizingAstVisitor { - final directives = <UriBasedDirective>[]; - - visitUriBasedDirective(UriBasedDirective node) => directives.add(node); -} diff --git a/lib/src/barback/transformers_needed_by_transformers.dart b/lib/src/barback/transformers_needed_by_transformers.dart new file mode 100644 index 0000000000000000000000000000000000000000..26bac5371f162bab0ac18b1590739b46bfd790d2 --- /dev/null +++ b/lib/src/barback/transformers_needed_by_transformers.dart @@ -0,0 +1,389 @@ +// 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.transformers_needed_by_transformers; + +import 'package:path/path.dart' as p; + +import '../barback.dart'; +import '../dart.dart'; +import '../io.dart'; +import '../package.dart'; +import '../package_graph.dart'; +import '../utils.dart'; +import 'cycle_exception.dart'; + +/// Returns a dependency graph for transformers in [graph]. +/// +/// 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>> computeTransformersNeededByTransformers( + PackageGraph graph) { + var result = {}; + var computer = new _DependencyComputer(graph); + for (var packageName in ordered(graph.packages.keys)) { + var package = graph.packages[packageName]; + for (var phase in package.pubspec.transformers) { + for (var id in phase) { + if (id.isBuiltInTransformer) continue; + result[id] = computer.transformersNeededByTransformer(id); + } + } + } + return result; +} + +/// A helper class for [computeTransformersNeededByTransformers] that keeps +/// package-graph-wide state and caches over the course of the computation. +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>>(); + + _DependencyComputer(this._graph) { + ordered(_graph.packages.keys).forEach(_loadPackageComputer); + } + + /// 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) { + // TODO(nweiz): We can do some pre-processing on the package graph (akin to + // the old ordering dependency computation) to figure out which packages are + // guaranteed not to require any transformers. That'll let us avoid extra + // work here and in [transformersNeededByPackage]. + + var components = p.split(p.fromUri(packageUri.path)); + var packageName = components.first; + 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 = p.join(package.dir, '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 (_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 id in phase) { + 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(); + + /// 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 id in phase) { + 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(id.getFullPath(_package.dir)); + } + } 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(id.getFullPath(_package.dir)); + 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.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 = p.join(_package.dir, '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(p.relative(library, from: _package.dir)).path; + if (_applicableTransformers.any((id) => id.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]; + } +} diff --git a/lib/src/dart.dart b/lib/src/dart.dart index 64374db6774f06e3e7b94c9bcec6d24cd3271390..9a8510568c66d70f3fd0a814d5500cdcf78187c7 100644 --- a/lib/src/dart.dart +++ b/lib/src/dart.dart @@ -122,6 +122,22 @@ bool isEntrypoint(CompilationUnit dart) { }); } +/// Efficiently parses the import and export directives in [contents]. +/// +/// If [name] is passed, it's used as the filename for error reporting. +List<UriBasedDirective> parseImportsAndExports(String contents, {String name}) { + var collector = new _DirectiveCollector(); + parseDirectives(contents, name: name).accept(collector); + return collector.directives; +} + +/// A simple visitor that collects import and export nodes. +class _DirectiveCollector extends GeneralizingAstVisitor { + final directives = <UriBasedDirective>[]; + + visitUriBasedDirective(UriBasedDirective node) => directives.add(node); +} + /// Runs [code] in an isolate. /// /// [code] should be the contents of a Dart entrypoint. It may contain imports; diff --git a/lib/src/package_graph.dart b/lib/src/package_graph.dart index 47e1349bfec2cd9679e2844ca483dc6b08e7a18f..14913787b95be9a5c9a70ad98a672c5762a03332 100644 --- a/lib/src/package_graph.dart +++ b/lib/src/package_graph.dart @@ -25,22 +25,4 @@ class PackageGraph { final Map<String, Package> packages; PackageGraph(this.entrypoint, this.lockFile, this.packages); - - /// Returns the set of transitive dependencies of the package named - /// [packageName]. - Set<Package> transitiveDependencies(String packageName) { - var seen = new Set<Package>(); - traverse(Package package) { - if (seen.contains(package)) return; - seen.add(package); - for (var dep in package.dependencies) { - traverse(packages[dep.name]); - } - } - - var package = packages[packageName]; - traverse(package); - seen.remove(package); - return seen; - } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 0ec5e46286bcedbe52368558ac49653d55e56519..8ebc8fe8ec62ebeda14d1fe25ccbcf7dda6cb9d1 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -6,7 +6,6 @@ library pub.utils; import 'dart:async'; -import "dart:collection"; import "dart:convert"; import 'dart:io'; import 'dart:isolate'; @@ -329,69 +328,9 @@ Future<Map> mapMapAsync(Map map, {key(key, value), value(key, value)}) { value: (mapKey) => value(mapKey, map[mapKey])); } -/// Returns the shortest path from [start] to [end] in [graph]. -/// -/// The graph is represented by a map where each key is a vertex and the value -/// is the set of other vertices directly reachable from the key. [start] and -/// [end] must be vertices in this graph. -List shortestPath(Map<dynamic, Iterable> graph, start, end) { - assert(graph.containsKey(start)); - assert(graph.containsKey(end)); - - // Dijkstra's algorithm. - var infinity = graph.length; - var distance = mapMap(graph, value: (_1, _2) => infinity); - - // A map from each node to the node that came before it on the shortest path - // from it back to [start]. - var previous = {}; - - distance[start] = 0; - var remaining = graph.keys.toSet(); - while (!remaining.isEmpty) { - var current = minBy(remaining, (node) => distance[node]); - remaining.remove(current); - - // If there's no remaining node that's reachable from [start], then there's - // no path from [start] to [end]. - if (distance[current] == infinity) return null; - - // If we've reached [end], we've found the shortest path to it and we just - // need to reconstruct that path. - if (current == end) break; - - for (var neighbor in graph[current]) { - if (!remaining.contains(neighbor)) continue; - var newDistance = distance[current] + 1; - if (newDistance >= distance[neighbor]) continue; - distance[neighbor] = newDistance; - previous[neighbor] = current; - } - } - - var path = new Queue(); - var current = end; - while (current != null) { - path.addFirst(current); - current = previous[current]; - } - - return path.toList(); -} - -/// Returns a copy of [graph] with all the edges reversed. -/// -/// The graph is represented by a map where each key is a vertex and the value -/// is the set of other vertices directly reachable from the key. -Map<dynamic, Set> reverseGraph(Map<dynamic, Set> graph) { - var reversed = new Map.fromIterable(graph.keys, value: (_) => new Set()); - graph.forEach((vertex, edges) { - for (var edge in edges) { - reversed[edge].add(vertex); - } - }); - return reversed; -} +/// Returns the maximum value in [iter]. +int maxAll(Iterable<int> iter) => + iter.reduce((max, element) => element > max ? element : max); /// Replace each instance of [matcher] in [source] with the return value of /// [fn]. diff --git a/test/test_pub.dart b/test/test_pub.dart index 9a6a7e6c42faac9f6a57ad5112b8d2770bf8b411..b89a02729bff21948b283aaacc6120d183d7d8a6 100644 --- a/test/test_pub.dart +++ b/test/test_pub.dart @@ -287,6 +287,11 @@ String yaml(value) => JSON.encode(value); String get sandboxDir => _sandboxDir; String _sandboxDir; +/// The path to the Dart repo's packages. +final String pkgPath = path.absolute(path.join( + path.dirname(Platform.executable), + '..', '..', '..', '..', 'pkg')); + /// The path of the package cache directory used for tests. Relative to the /// sandbox directory. final String cachePath = "cache"; @@ -673,10 +678,6 @@ void createLockFile(String package, {Iterable<String> sandbox, } if (pkg != null) { - var pkgDir = path.absolute(path.join( - path.dirname(Platform.executable), - '..', '..', '..', '..', 'pkg')); - _addPackage(String package) { if (dependencies.containsKey(package)) return; @@ -689,7 +690,7 @@ void createLockFile(String package, {Iterable<String> sandbox, } packagePath = _barbackDir; } else { - packagePath = path.join(pkgDir, package); + packagePath = path.join(pkgPath, package); } dependencies[package] = packagePath; diff --git a/test/transformer/detects_a_transformer_cycle_test.dart b/test/transformer/detects_a_transformer_cycle_test.dart deleted file mode 100644 index df8c0baabb2443f70f0235f3748f0b46f984c40c..0000000000000000000000000000000000000000 --- a/test/transformer/detects_a_transformer_cycle_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS d.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_tests; - -import '../descriptor.dart' as d; -import '../test_pub.dart'; -import '../serve/utils.dart'; - -main() { - initConfig(); - withBarbackVersions("any", () { - integration("detects a transformer cycle", () { - d.dir("foo", [ - d.pubspec({ - "name": "foo", - "version": "1.0.0", - "transformers": ["myapp/transformer"], - "dependencies": {'myapp': {'path': '../myapp'}} - }), - d.dir("lib", [ - d.file("transformer.dart", dartTransformer('foo')), - ]) - ]).create(); - - d.dir(appPath, [ - d.pubspec({ - "name": "myapp", - "transformers": ["foo/transformer"], - "dependencies": {'foo': {'path': '../foo'}} - }), - d.dir("lib", [ - d.file("transformer.dart", dartTransformer('myapp')), - ]) - ]).create(); - - createLockFile('myapp', sandbox: ['foo'], pkg: ['barback']); - - var process = startPubServe(); - process.shouldExit(1); - process.stderr.expect(emitsLines( - "Transformer cycle detected:\n" - " foo is transformed by myapp/transformer\n" - " myapp is transformed by foo/transformer")); - }); - }); -} diff --git a/test/transformer/detects_an_ordering_dependency_cycle_test.dart b/test/transformer/detects_an_ordering_dependency_cycle_test.dart deleted file mode 100644 index fe973e8ddccb330f1269db61f5c2fa3339c287ef..0000000000000000000000000000000000000000 --- a/test/transformer/detects_an_ordering_dependency_cycle_test.dart +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS d.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_tests; - -import '../descriptor.dart' as d; -import '../test_pub.dart'; -import '../serve/utils.dart'; - -main() { - initConfig(); - withBarbackVersions("any", () { - integration("detects an ordering dependency cycle", () { - d.dir("foo", [ - d.pubspec({ - "name": "foo", - "version": "1.0.0", - "transformers": ["myapp/transformer"], - "dependencies": {'myapp': {'path': '../myapp'}} - }) - ]).create(); - - d.dir("bar", [ - d.pubspec({ - "name": "bar", - "version": "1.0.0", - "dependencies": {'foo': {'path': '../foo'}} - }), - d.dir("lib", [ - d.file("transformer.dart", dartTransformer('bar')), - ]) - ]).create(); - - d.dir("baz", [ - d.pubspec({ - "name": "baz", - "version": "1.0.0", - "transformers": ["bar/transformer"], - "dependencies": {'bar': {'path': '../bar'}} - }) - ]).create(); - - d.dir(appPath, [ - d.pubspec({ - "name": "myapp", - "dependencies": {'baz': {'path': '../baz'}} - }), - d.dir("lib", [ - d.file("transformer.dart", dartTransformer('myapp')), - ]) - ]).create(); - - createLockFile('myapp', sandbox: ['foo', 'bar', 'baz'], pkg: ['barback']); - - var process = startPubServe(); - process.shouldExit(1); - process.stderr.expect(emitsLines( - "Transformer cycle detected:\n" - " bar depends on foo\n" - " foo is transformed by myapp/transformer\n" - " myapp depends on baz\n" - " baz is transformed by bar/transformer")); - }); - }); -} diff --git a/test/transformer/loads_ordering_dependencies_in_the_correct_order_test.dart b/test/transformer/loads_ordering_dependencies_in_the_correct_order_test.dart deleted file mode 100644 index 7689b6af47e56b60b221c8f040e5b6db80d88af9..0000000000000000000000000000000000000000 --- a/test/transformer/loads_ordering_dependencies_in_the_correct_order_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS d.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_tests; - -import '../descriptor.dart' as d; -import '../test_pub.dart'; -import '../serve/utils.dart'; - -main() { - initConfig(); - withBarbackVersions("any", () { - integration("loads ordering dependencies in the correct order", () { - d.dir("foo", [ - d.libPubspec("foo", '1.0.0'), - d.dir("lib", [ - d.file("transformer.dart", dartTransformer('foo')) - ]) - ]).create(); - - d.dir("bar", [ - d.pubspec({ - "name": "bar", - "version": "1.0.0", - "transformers": ["foo/transformer"], - "dependencies": {"foo": {"path": "../foo"}} - }), - d.dir("lib", [ - d.file("bar.dart", 'const TOKEN = "bar";') - ]) - ]).create(); - - d.dir(appPath, [ - d.pubspec({ - "name": "myapp", - "transformers": ["myapp/transformer"], - "dependencies": {"bar": {"path": "../bar"}} - }), - d.dir("lib", [ - d.file("transformer.dart", dartTransformer('myapp', import: 'bar')) - ]), - d.dir("web", [ - d.file("main.dart", 'const TOKEN = "main.dart";') - ]) - ]).create(); - - createLockFile('myapp', sandbox: ['foo', 'bar'], pkg: ['barback']); - - pubServe(); - requestShouldSucceed("main.dart", - 'const TOKEN = "(main.dart, myapp imports (bar, foo))";'); - endPubServe(); - }); - }); -} diff --git a/test/transformers_needed_by_transformers/conservative_dependencies_test.dart b/test/transformers_needed_by_transformers/conservative_dependencies_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..19118a6d5965e9b0ec223853b269bdcaa800d966 --- /dev/null +++ b/test/transformers_needed_by_transformers/conservative_dependencies_test.dart @@ -0,0 +1,466 @@ +// 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_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +void main() { + initConfig(); + + integration("reports previous transformers as dependencies if the " + "transformer is transformed", () { + // The root app just exists so that something is transformed by pkg and qux. + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "version": "1.0.0", + "dependencies": { + "pkg": {"path": "../pkg"}, + "qux": {"path": "../qux"} + }, + "transformers": ["pkg", "qux"] + }) + ]).create(); + + d.dir("pkg", [ + d.pubspec({ + "name": "pkg", + "version": "1.0.0", + "dependencies": { + "foo": {"path": "../foo"}, + "bar": {"path": "../bar"}, + "baz": {"path": "../baz"}, + }, + "transformers": [ + {"foo": {"\$include": "lib/pkg.dart"}}, + {"bar": {"\$exclude": "lib/transformer.dart"}}, + "baz" + ] + }), + d.dir("lib", [ + d.file("pkg.dart", ""), + d.file("transformer.dart", transformer()) + ]) + ]).create(); + + // Even though foo and bar don't modify pkg/lib/transformer.dart themselves, + // it may be modified to import a library that they modify or generate, so + // pkg will depend on them. + d.dir("foo", [ + d.libPubspec("foo", "1.0.0"), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + d.dir("bar", [ + d.libPubspec("bar", "1.0.0"), + d.dir("lib", [d.file("bar.dart", transformer())]) + ]).create(); + + // baz transforms pkg/lib/transformer.dart, so pkg will obviously + // depend on it. + d.dir("baz", [ + d.libPubspec("baz", "1.0.0"), + d.dir("lib", [d.file("baz.dart", transformer())]) + ]).create(); + + // qux doesn't transform anything in pkg, so pkg won't depend on it. + d.dir("qux", [ + d.libPubspec("qux", "1.0.0"), + d.dir("lib", [d.file("qux.dart", transformer())]) + ]).create(); + + expectDependencies({ + 'pkg': ['foo', 'bar', 'baz'], 'foo': [], 'bar': [], 'baz': [], 'qux': [] + }); + }); + + integration("reports all transitive package dependencies' transformers as " + "dependencies if the transformer is transformed", () { + // The root app just exists so that something is transformed by pkg and qux. + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": { + "pkg": {"path": "../pkg"}, + "qux": {"path": "../qux"} + }, + "transformers": ["pkg"] + }) + ]).create(); + + d.dir("pkg", [ + d.pubspec({ + "name": "pkg", + "version": "1.0.0", + "dependencies": { + "foo": {"path": "../foo"}, + "baz": {"path": "../baz"} + }, + "transformers": ["baz"] + }), + d.dir("lib", [d.file("pkg.dart", transformer())]) + ]).create(); + + // pkg depends on foo. Even though it's not transformed by foo, its + // transformed transformer could import foo, so it has to depend on foo. + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "dependencies": {"bar": {"path": "../bar"}}, + "transformers": ["foo"] + }), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + // foo depends on bar, and like pkg's dependency on foo, the transformed + // version of foo's transformer could import bar, so foo has to depend on + // bar. + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "version": "1.0.0", + "transformers": ["bar"] + }), + d.dir("lib", [d.file("bar.dart", transformer())]) + ]).create(); + + /// foo is transformed by baz. + d.dir("baz", [ + d.libPubspec("baz", "1.0.0"), + d.dir("lib", [d.file("baz.dart", transformer())]) + ]).create(); + + /// qux is not part of pkg's transitive dependency tree, so pkg shouldn't + /// depend on it. + d.dir("qux", [ + d.pubspec({ + "name": "qux", + "version": "1.0.0", + "transformers": ["qux"] + }), + d.dir("lib", [d.file("qux.dart", transformer())]) + ]).create(); + + expectDependencies({ + 'pkg': ['foo', 'bar', 'baz'], 'foo': [], 'bar': [], 'baz': [], 'qux': [] + }); + }); + + integration("reports previous transformers as dependencies if a " + "nonexistent local file is imported", () { + // The root app just exists so that something is transformed by pkg and bar. + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": { + "pkg": {"path": "../pkg"}, + "bar": {"path": "../bar"} + }, + "transformers": ["pkg", "bar"] + }) + ]).create(); + + d.dir("pkg", [ + d.pubspec({ + "name": "pkg", + "version": "1.0.0", + "dependencies": { + "foo": {"path": "../foo"}, + "bar": {"path": "../bar"} + }, + "transformers": [{"foo": {"\$include": "lib/pkg.dart"}}] + }), + d.dir("lib", [ + d.file("pkg.dart", ""), + d.file("transformer.dart", transformer(["nonexistent.dart"])) + ]) + ]).create(); + + // Since pkg's transformer imports a nonexistent file, we assume that file + // was generated by foo's transformer. Thus pkg's transformer depends on + // foo's even though the latter doesn't transform the former. + d.dir("foo", [ + d.libPubspec("foo", "1.0.0"), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + /// qux is not part of pkg's transitive dependency tree, so pkg shouldn't + /// depend on it. + d.dir("bar", [ + d.libPubspec("bar", "1.0.0"), + d.dir("lib", [d.file("bar.dart", transformer())]) + ]).create(); + + expectDependencies({'pkg': ['foo'], 'foo': [], 'bar': []}); + }); + + integration("reports all that package's dependencies' transformers as " + "dependencies if a non-existent file is imported from another package", + () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": { + "foo": {"path": "../foo"}, + "qux": {"path": "../qux"} + }, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", transformer(["package:foo/nonexistent.dart"])) + ]) + ]).create(); + + // myapp imported a nonexistent file from foo so myapp will depend on every + // transformer transitively reachable from foo, since the nonexistent file + // could be generated to import anything. + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "dependencies": { + "bar": {"path": "../bar"}, + "baz": {"path": "../baz"} + }, + "transformers": ["foo"] + }), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + // bar is a dependency of foo so myapp will depend on it. + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "version": "1.0.0", + "transformers": ["bar"] + }), + d.dir("lib", [d.file("bar.dart", transformer())]) + ]).create(); + + // baz is a dependency of foo so myapp will depend on it. + d.dir("baz", [ + d.pubspec({ + "name": "baz", + "version": "1.0.0", + "transformers": ["baz"] + }), + d.dir("lib", [d.file("baz.dart", transformer())]) + ]).create(); + + // qux is not transitively reachable from foo so myapp won't depend on it. + d.dir("qux", [ + d.pubspec({ + "name": "qux", + "version": "1.0.0", + "transformers": ["qux"] + }), + d.dir("lib", [d.file("qux.dart", transformer())]) + ]).create(); + + expectDependencies({ + 'myapp': ['foo', 'bar', 'baz'], 'foo': [], 'bar': [], 'baz': [], 'qux': [] + }); + }); + + integration("reports all that package's dependencies' transformers as " + "dependencies if a non-existent transformer is used from another package", + () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": { + "foo": {"path": "../foo"}, + "qux": {"path": "../qux"} + }, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", transformer(["package:foo/nonexistent.dart"])) + ]) + ]).create(); + + // myapp imported a nonexistent file from foo so myapp will depend on every + // transformer transitively reachable from foo, since the nonexistent file + // could be generated to import anything. + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "dependencies": { + "bar": {"path": "../bar"}, + "baz": {"path": "../baz"} + }, + "transformers": ["bar"] + }) + ]).create(); + + // bar is a dependency of foo so myapp will depend on it. + d.dir("bar", [ + d.libPubspec("bar", "1.0.0"), + d.dir("lib", [d.file("bar.dart", transformer())]) + ]).create(); + + // baz is a dependency of foo so myapp will depend on it. + d.dir("baz", [ + d.pubspec({ + "name": "baz", + "version": "1.0.0", + "transformers": ["baz"] + }), + d.dir("lib", [d.file("baz.dart", transformer())]) + ]).create(); + + // qux is not transitively reachable from foo so myapp won't depend on it. + d.dir("qux", [ + d.pubspec({ + "name": "qux", + "version": "1.0.0", + "transformers": ["qux"] + }), + d.dir("lib", [d.file("qux.dart", transformer())]) + ]).create(); + + expectDependencies({ + 'myapp': ['bar', 'baz'], 'bar': [], 'baz': [], 'qux': [] + }); + }); + + test("reports dependencies on transformers in past phases", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + "myapp/first", + "myapp/second", + "myapp/third" + ] + }), + d.dir("lib", [ + d.file("first.dart", transformer()), + d.file("second.dart", transformer()), + d.file("third.dart", transformer()) + ]) + ]).create(); + + expectDependencies({ + 'myapp/first': [], + 'myapp/second': ['myapp/first'], + 'myapp/third': ['myapp/second', 'myapp/first'] + }); + }); + + integration("considers the entrypoint package's dev and override " + "dependencies", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "dev_dependencies": {"bar": {"path": "../bar"}}, + "dependency_overrides": {"baz": {"path": "../baz"}}, + "transformers": ["foo", "myapp"] + }), + d.dir("lib", [d.file("myapp.dart", transformer())]) + ]).create(); + + // foo transforms myapp's transformer so it could import from bar or baz. + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "transformers": ["foo"] + }), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + // bar is a dev dependency that myapp could import from, so myapp should + // depend on it. + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "version": "1.0.0", + "transformers": ["bar"] + }), + d.dir("lib", [d.file("bar.dart", transformer())]) + ]).create(); + + // baz is an override dependency that myapp could import from, so myapp + // should depend on it. + d.dir("baz", [ + d.pubspec({ + "name": "baz", + "version": "1.0.0", + "transformers": ["baz"] + }), + d.dir("lib", [d.file("baz.dart", transformer())]) + ]).create(); + + expectDependencies({ + 'myapp': ['foo', 'bar', 'baz'], 'foo': [], 'bar': [], 'baz': [] + }); + }); + + integration("doesn't consider a non-entrypoint package's dev and override " + "dependencies", () { + // myapp just exists so that pkg isn't the entrypoint. + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"pkg": {"path": "../pkg"}} + }) + ]).create(); + + d.dir("pkg", [ + d.pubspec({ + "name": "pkg", + "dependencies": {"foo": {"path": "../foo"}}, + "dev_dependencies": {"bar": {"path": "../bar"}}, + "dependency_overrides": {"baz": {"path": "../baz"}}, + "transformers": ["foo", "pkg"] + }), + d.dir("lib", [d.file("pkg.dart", transformer())]) + ]).create(); + + // foo transforms pkg's transformer so it could theoretcially import from + // bar or baz. However, since pkg isn't the entrypoint, it doesn't have + // access to them. + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "transformers": ["foo"] + }), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + // bar is a dev dependency that myapp can't import from, so myapp shouldn't + // depend on it. + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "version": "1.0.0", + "transformers": ["bar"] + }), + d.dir("lib", [d.file("bar.dart", transformer())]) + ]).create(); + + // baz is a dev dependency that myapp can't import from, so myapp shouldn't + // depend on it. + d.dir("baz", [ + d.pubspec({ + "name": "baz", + "version": "1.0.0", + "transformers": ["baz"] + }), + d.dir("lib", [d.file("baz.dart", transformer())]) + ]).create(); + + expectDependencies({'pkg': ['foo'], 'foo': [], 'bar': [], 'baz': []}); + }); +} \ No newline at end of file diff --git a/test/transformers_needed_by_transformers/cycle_test.dart b/test/transformers_needed_by_transformers/cycle_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..a8458c1499b8380f657a3965f7bfcfc402dc3a39 --- /dev/null +++ b/test/transformers_needed_by_transformers/cycle_test.dart @@ -0,0 +1,212 @@ +// 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_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +void main() { + initConfig(); + + integration("allows a package dependency cycle that's unrelated to " + "transformers", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp/first", "myapp/second"] + }), + d.dir('lib', [ + d.file("first.dart", transformer()), + d.file("second.dart", transformer()) + ]) + ]).create(); + + d.dir("foo", [ + d.libPubspec("foo", "1.0.0", deps: {"bar": {"path": "../bar"}}) + ]).create(); + + d.dir("bar", [ + d.libPubspec("bar", "1.0.0", deps: {"baz": {"path": "../baz"}}) + ]).create(); + + d.dir("baz", [ + d.libPubspec("baz", "1.0.0", deps: {"foo": {"path": "../foo"}}) + ]).create(); + + expectDependencies({'myapp/first': [], 'myapp/second': ['myapp/first']}); + }); + + integration("disallows a package dependency cycle that may be related to " + "transformers", () { + // Two layers of myapp transformers are necessary here because otherwise pub + // will figure out that the transformer doesn't import "foo" and thus + // doesn't transitively import itself. Import loops are tested below. + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp/first", "myapp/second"] + }), + d.dir('lib', [ + d.file("first.dart", transformer()), + d.file("second.dart", transformer()) + ]) + ]).create(); + + d.dir("foo", [ + d.libPubspec("foo", "1.0.0", deps: {"bar": {"path": "../bar"}}) + ]).create(); + + d.dir("bar", [ + d.libPubspec("bar", "1.0.0", deps: {"myapp": {"path": "../myapp"}}) + ]).create(); + + expectCycleException([ + "myapp is transformed by myapp/second", + "myapp depends on foo", + "foo depends on bar", + "bar depends on myapp", + "myapp is transformed by myapp/first" + ]); + }); + + integration("disallows a transformation dependency cycle", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["foo"] + }), + d.dir('lib', [d.file("myapp.dart", transformer())]) + ]).create(); + + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "dependencies": {"bar": {"path": "../bar"}}, + "transformers": ["bar"] + }), + d.dir('lib', [d.file("foo.dart", transformer())]) + ]).create(); + + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "dependencies": {"myapp": {"path": "../myapp"}}, + "transformers": ["myapp"] + }), + d.dir('lib', [d.file("bar.dart", transformer())]) + ]).create(); + + expectCycleException([ + "bar is transformed by myapp", + "myapp is transformed by foo", + "foo is transformed by bar" + ]); + }); + + integration("allows a cross-package import cycle that's unrelated to " + "transformers", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir('lib', [ + d.file("myapp.dart", transformer(['package:foo/foo.dart'])) + ]) + ]).create(); + + d.dir("foo", [ + d.libPubspec("foo", "1.0.0", deps: {"bar": {"path": "../bar"}}), + d.dir('lib', [d.file("foo.dart", "import 'package:bar/bar.dart';")]) + ]).create(); + + d.dir("bar", [ + d.libPubspec("bar", "1.0.0", deps: {"baz": {"path": "../baz"}}), + d.dir('lib', [d.file("bar.dart", "import 'package:baz/baz.dart';")]) + ]).create(); + + d.dir("baz", [ + d.libPubspec("baz", "1.0.0", deps: {"foo": {"path": "../foo"}}), + d.dir('lib', [d.file("baz.dart", "import 'package:foo/foo.dart';")]) + ]).create(); + + expectDependencies({'myapp': []}); + }); + + integration("disallows a cross-package import cycle that's related to " + "transformers", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir('lib', [ + d.file("myapp.dart", transformer(['package:foo/foo.dart'])) + ]) + ]).create(); + + d.dir("foo", [ + d.libPubspec("foo", "1.0.0", deps: {"bar": {"path": "../bar"}}), + d.dir('lib', [d.file("foo.dart", "import 'package:bar/bar.dart';")]) + ]).create(); + + d.dir("bar", [ + d.libPubspec("bar", "1.0.0", deps: {"myapp": {"path": "../myapp"}}), + d.dir('lib', [d.file("bar.dart", "import 'package:myapp/myapp.dart';")]) + ]).create(); + + expectCycleException([ + "myapp is transformed by myapp", + "myapp depends on foo", + "foo depends on bar", + "bar depends on myapp" + ]); + }); + + integration("allows a single-package import cycle that's unrelated to " + "transformers", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir('lib', [ + d.file("myapp.dart", transformer(['foo.dart'])), + d.file("foo.dart", "import 'bar.dart';"), + d.file("bar.dart", "import 'baz.dart';"), + d.file("baz.dart", "import 'foo.dart';") + ]) + ]).create(); + + expectDependencies({'myapp': []}); + }); + + integration("allows a single-package import cycle that's related to " + "transformers", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir('lib', [ + d.file("myapp.dart", transformer(['foo.dart'])), + d.file("foo.dart", "import 'bar.dart';"), + d.file("bar.dart", "import 'myapp.dart';"), + ]) + ]).create(); + + expectDependencies({'myapp': []}); + }); +} \ No newline at end of file diff --git a/test/transformers_needed_by_transformers/error_test.dart b/test/transformers_needed_by_transformers/error_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..0908cb4f917df710e3c807a852933798f771bae6 --- /dev/null +++ b/test/transformers_needed_by_transformers/error_test.dart @@ -0,0 +1,49 @@ +// 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_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +void main() { + initConfig(); + + integration("fails if an unknown package is imported", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": ["myapp"] + }), + d.dir('lib', [ + d.file("myapp.dart", transformer(["package:foo/foo.dart"])) + ]) + ]).create(); + + expectException(predicate((error) { + expect(error, new isInstanceOf<ApplicationException>()); + expect(error.message, equals( + 'A transformer imported unknown package "foo" (in ' + '"package:foo/foo.dart").')); + return true; + })); + }); + + integration("fails on a syntax error", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": ["myapp"] + }), + d.dir('lib', [ + d.file("myapp.dart", "library;") + ]) + ]).create(); + + expectException(new isInstanceOf<AnalyzerErrorGroup>()); + }); +} diff --git a/test/transformers_needed_by_transformers/import_dependencies_test.dart b/test/transformers_needed_by_transformers/import_dependencies_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..342425e6a3d27da8ba46e489bb5f12cd0d1cafd0 --- /dev/null +++ b/test/transformers_needed_by_transformers/import_dependencies_test.dart @@ -0,0 +1,195 @@ +// 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_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +void main() { + initConfig(); + + integration("reports a dependency if a transformed local file is imported", + () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": [ + {"foo": {"\$include": "lib/lib.dart"}}, + "myapp" + ] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("lib.dart", ""), + d.file("transformer.dart", transformer(["lib.dart"])) + ]) + ]).create(); + + d.dir("foo", [ + d.pubspec({"name": "foo", "version": "1.0.0"}), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + expectDependencies({'myapp': ['foo'], 'foo': []}); + }); + + integration("reports a dependency if a transformed foreign file is imported", + () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("transformer.dart", transformer(["package:foo/foo.dart"])) + ]) + ]).create(); + + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "transformers": [{"foo": {"\$include": "lib/foo.dart"}}] + }), + d.dir("lib", [ + d.file("foo.dart", ""), + d.file("transformer.dart", transformer()) + ]) + ]).create(); + + expectDependencies({'myapp': ['foo'], 'foo': []}); + }); + + integration("reports a dependency if a transformed external package file is " + "imported from an export", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("transformer.dart", transformer(["local.dart"])), + d.file("local.dart", "export 'package:foo/foo.dart';") + ]) + ]).create(); + + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "transformers": [{"foo": {"\$include": "lib/foo.dart"}}] + }), + d.dir("lib", [ + d.file("foo.dart", ""), + d.file("transformer.dart", transformer()) + ]) + ]).create(); + + expectDependencies({'myapp': ['foo'], 'foo': []}); + }); + + integration("reports a dependency if a transformed foreign file is " + "transitively imported", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("transformer.dart", transformer(["local.dart"])), + d.file("local.dart", "import 'package:foo/foreign.dart';") + ]) + ]).create(); + + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "transformers": [{"foo": {"\$include": "lib/foo.dart"}}] + }), + d.dir("lib", [ + d.file("foo.dart", ""), + d.file("transformer.dart", transformer()), + d.file("foreign.dart", "import 'foo.dart';") + ]) + ]).create(); + + expectDependencies({'myapp': ['foo'], 'foo': []}); + }); + + integration("reports a dependency if a transformed foreign file is " + "transitively imported across packages", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("transformer.dart", transformer(["package:foo/foo.dart"])), + ]) + ]).create(); + + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "dependencies": {"bar": {"path": "../bar"}} + }), + d.dir("lib", [d.file("foo.dart", "import 'package:bar/bar.dart';")]) + ]).create(); + + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "version": "1.0.0", + "transformers": [{"bar": {"\$include": "lib/bar.dart"}}] + }), + d.dir("lib", [ + d.file("bar.dart", ""), + d.file("transformer.dart", transformer()) + ]) + ]).create(); + + expectDependencies({'myapp': ['bar'], 'bar': []}); + }); + + integration("reports a dependency if an imported file is transformed by a " + "different package", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": [ + {"foo": {'\$include': 'lib/local.dart'}}, + "myapp" + ] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("transformer.dart", transformer(["local.dart"])), + d.file("local.dart", "") + ]) + ]).create(); + + d.dir("foo", [ + d.pubspec({"name": "foo", "version": "1.0.0"}), + d.dir("lib", [d.file("transformer.dart", transformer())]) + ]).create(); + + expectDependencies({'myapp': ['foo'], 'foo': []}); + }); +} \ No newline at end of file diff --git a/test/transformers_needed_by_transformers/no_dependencies_test.dart b/test/transformers_needed_by_transformers/no_dependencies_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..c28e4a0ae694329fd29a26a292f5303ee30cbb85 --- /dev/null +++ b/test/transformers_needed_by_transformers/no_dependencies_test.dart @@ -0,0 +1,161 @@ +// 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_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +void main() { + initConfig(); + + integration("reports no dependencies if no transformers are used", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}} + }) + ]).create(); + + d.dir("foo", [d.libPubspec("foo", "1.0.0")]).create(); + + expectDependencies({}); + }); + + integration("reports no dependencies if a transformer is used in a " + "package that doesn't expose a transformer", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["foo"] + }) + ]).create(); + + d.dir("foo", [ + d.libPubspec("foo", "1.0.0"), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + expectDependencies({"foo": []}); + }); + + integration("reports no dependencies for non-file/package imports", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", transformer([ + "dart:async", + "http://dartlang.org/nonexistent.dart" + ])) + ]) + ]).create(); + + d.dir("foo", [ + d.libPubspec("foo", "1.0.0"), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + expectDependencies({"myapp": []}); + }); + + integration("reports no dependencies for a single self transformer", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": ["myapp"] + }), + d.dir("lib", [d.file("myapp.dart", transformer())]) + ]).create(); + + expectDependencies({"myapp": []}); + }); + + integration("reports no dependencies if a transformer applies to files that " + "aren't used by the exposed transformer", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": [ + {"foo": {"\$include": "lib/myapp.dart"}}, + {"foo": {"\$exclude": "lib/transformer.dart"}}, + "myapp" + ] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("transformer.dart", transformer()) + ]) + ]).create(); + + d.dir("foo", [ + d.libPubspec("foo", "1.0.0"), + d.dir("lib", [d.file("foo.dart", transformer())]) + ]).create(); + + expectDependencies({"myapp": [], "foo": []}); + }); + + integration("reports no dependencies if a transformer applies to a " + "dependency's files that aren't used by the exposed transformer", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "dependencies": {"foo": {"path": "../foo"}}, + "transformers": ["myapp"] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("transformer.dart", transformer(["package:foo/foo.dart"])) + ]) + ]).create(); + + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "version": "1.0.0", + "transformers": [{"foo": {"\$exclude": "lib/foo.dart"}}] + }), + d.dir("lib", [ + d.file("foo.dart", ""), + d.file("transformer.dart", transformer()) + ]) + ]).create(); + + expectDependencies({'myapp': [], 'foo': []}); + }); + + test("reports no dependencies on transformers in future phases", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + {"myapp/first": {"\$include": "lib/myapp.dart"}}, + {"myapp/second": {"\$include": "lib/first.dart"}}, + {"myapp/third": {"\$include": "lib/second.dart"}} + ] + }), + d.dir("lib", [ + d.file("myapp.dart", ""), + d.file("first.dart", transformer()), + d.file("second.dart", transformer()), + d.file("third.dart", transformer()) + ]) + ]).create(); + + expectDependencies({ + 'myapp/first': [], + 'myapp/second': [], + 'myapp/third': [] + }); + }); +} diff --git a/test/transformers_needed_by_transformers/utils.dart b/test/transformers_needed_by_transformers/utils.dart new file mode 100644 index 0000000000000000000000000000000000000000..47e1cf7894ed4af132ea576318a8b48a0b7f7603 --- /dev/null +++ b/test/transformers_needed_by_transformers/utils.dart @@ -0,0 +1,112 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../lib/src/barback/cycle_exception.dart'; +import '../../lib/src/barback/transformers_needed_by_transformers.dart'; +import '../../lib/src/entrypoint.dart'; +import '../../lib/src/io.dart'; +import '../../lib/src/package.dart'; +import '../../lib/src/package_graph.dart'; +import '../../lib/src/source/path.dart'; +import '../../lib/src/system_cache.dart'; +import '../../lib/src/utils.dart'; +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +/// Expects that [computeTransformersNeededByTransformers] will return a graph +/// matching [expected] when run on the package graph defined by packages in +/// the sandbox. +void expectDependencies(Map<String, Iterable<String>> expected) { + expected = mapMap(expected, value: (_, ids) => ids.toSet()); + + schedule(() { + var result = mapMap( + computeTransformersNeededByTransformers(_loadPackageGraph()), + key: (id, _) => id.toString(), + value: (_, ids) => ids.map((id) => id.toString()).toSet()); + expect(result, equals(expected)); + }, "expect dependencies to match $expected"); +} + +/// Expects that [computeTransformersNeededByTransformers] will throw an +/// exception matching [matcher] when run on the package graph defiend by +/// packages in the sandbox. +void expectException(matcher) { + schedule(() { + expect(() => computeTransformersNeededByTransformers(_loadPackageGraph()), + throwsA(matcher)); + }, "expect an exception: $matcher"); +} + +/// Expects that [computeTransformersNeededByTransformers] will throw a +/// [CycleException] with the given [steps] when run on the package graph +/// defiend by packages in the sandbox. +void expectCycleException(Iterable<String> steps) { + expectException(predicate((error) { + expect(error, new isInstanceOf<CycleException>()); + expect(error.steps, equals(steps)); + return true; + }, "cycle exception:\n${steps.map((step) => " $step").join("\n")}")); +} + +/// Loads a [PackageGraph] from the packages in the sandbox. +/// +/// This graph will also include barback and its transitive dependencies from +/// the repo. +PackageGraph _loadPackageGraph() { + // Load the sandbox packages. + var packages = {}; + + var systemCache = new SystemCache(p.join(sandboxDir, cachePath)); + systemCache.sources + ..register(new PathSource()) + ..setDefault('path'); + var entrypoint = new Entrypoint(p.join(sandboxDir, appPath), systemCache); + + for (var package in listDir(sandboxDir)) { + if (!fileExists(p.join(package, 'pubspec.yaml'))) continue; + var packageName = p.basename(package); + packages[packageName] = new Package.load( + packageName, package, systemCache.sources); + } + + loadPackage(packageName) { + if (packages.containsKey(packageName)) return; + packages[packageName] = new Package.load( + packageName, p.join(pkgPath, packageName), systemCache.sources); + for (var dep in packages[packageName].dependencies) { + loadPackage(dep.name); + } + } + + loadPackage('barback'); + + return new PackageGraph(entrypoint, null, packages); +} + +/// Returns the contents of a no-op transformer that imports each URL in +/// [imports]. +String transformer([Iterable<String> imports]) { + if (imports == null) imports = []; + + var buffer = new StringBuffer() + ..writeln('import "package:barback/barback.dart";'); + for (var import in imports) { + buffer.writeln('import "$import";'); + } + + buffer.writeln(""" +NoOpTransformer extends Transformer { + bool isPrimary(AssetId id) => true; + void apply(Transform transform) {} +} +"""); + + return buffer.toString(); +}