blob: 0e477b2f41b21c27e398449000b90eaa7423abf0 [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'dart:math';
import 'package:analyzer/analyzer.dart';
import 'package:barback/barback.dart';
import 'package:path/path.dart' as p;
import '../barback.dart';
import '../dart.dart' show isEntrypoint, isPart;
import '../io.dart';
import 'module.dart';
/// There are two "types" of modules, `public` and `private`.
///
/// The `public` mode requires that all files are under `lib`.
///
/// The `private` mode requires that no files are under `lib`. All files must
/// still live under some shared top level directory.
enum ModuleMode {
public,
private,
}
/// Computes the [Module]s for [srcAssets], or throws an [ArgumentError] if the
/// configuration is invalid.
///
/// All entrypoints are guaranteed their own [Module], unless they are in a
/// strongly connected component with another entrypoint in which case a
/// single [Module] is created for the strongly connected component.
///
/// If a source is not imported by any entrypoint then it will end up in its own
/// module, (or possibly merged into a strongly connected component module).
///
/// An entrypoint is defined as follows:
///
/// * In [ModuleMode.public], any asset under "lib" but not "lib/src".
///
/// * In [ModuleMode.private], any asset for which [isEntrypoint] returns
/// `true` (for the parsed ast).
///
/// It is guaranteed that no asset will be added to more than one [Module].
Future<List<Module>> computeModules(
ModuleMode mode, Iterable<Asset> srcAssets) async {
// Validate `srcAssets`, must be non-empty and all under the same dir.
if (srcAssets.isEmpty) {
throw new ArgumentError('Got unexpected empty `srcs`.');
}
var dir = topLevelDir(srcAssets.first.id.path);
if (!srcAssets.every((src) => topLevelDir(src.id.path) == dir)) {
throw new ArgumentError(
'All srcs must live in the same top level directory.');
}
// Validate that the `mode` and `srcAssets` agree.
switch (mode) {
case ModuleMode.public:
if (dir != 'lib') {
throw new ArgumentError(
'In `ModuleMode.public` all sources must be under `lib`, but the '
'given `srcs` are under `$dir`.');
}
break;
case ModuleMode.private:
if (dir == 'lib') {
throw new ArgumentError(
'In `ModuleMode.private` no sources may be under `lib`, but the '
'given `srcs` are.');
}
break;
}
// The set of entry points from `srcAssets` based on `mode`.
var entryIds = new Set<AssetId>();
// All the `srcAssets` that are part files.
var partIds = new Set<AssetId>();
// Invalid assets that should be removed from `srcAssets` after this loop.
var idsToRemove = <AssetId>[];
var parsedAssetsById = <AssetId, CompilationUnit>{};
for (var asset in srcAssets) {
var id = asset.id;
var content = await asset.readAsString();
// Skip errors here, dartdevc gives nicer messages.
var parsed = parseCompilationUnit(content,
name: id.path, parseFunctionBodies: false, suppressErrors: true);
parsedAssetsById[id] = parsed;
// Skip any files which contain a `dart:_` import.
if (parsed.directives.any((d) =>
d is UriBasedDirective && d.uri.stringValue.startsWith('dart:_'))) {
idsToRemove.add(asset.id);
continue;
}
// Short-circuit for part files.
if (isPart(parsed)) {
partIds.add(asset.id);
continue;
}
switch (mode) {
case ModuleMode.public:
if (!id.path.startsWith('lib/src/')) entryIds.add(id);
break;
case ModuleMode.private:
if (isEntrypoint(parsed)) entryIds.add(id);
break;
}
}
srcAssets = srcAssets.where((asset) => !idsToRemove.contains(asset.id));
// Build the `_AssetNode`s for each asset, skipping part files.
var nodesById = <AssetId, _AssetNode>{};
var srcAssetIds = srcAssets.map((asset) => asset.id).toSet();
var nonPartAssets = srcAssets.where((asset) => !partIds.contains(asset.id));
for (var asset in nonPartAssets) {
var node = new _AssetNode.forParsedUnit(
asset.id, parsedAssetsById[asset.id], srcAssetIds);
nodesById[asset.id] = node;
}
return new _ModuleComputer(entryIds, mode, nodesById)._computeModules();
}
/// An [AssetId] and all of its internal/external deps based on it's
/// [Directive]s.
///
/// Used to compute strongly connected components in the import graph for all
/// "internal" deps. Any "external" deps are ignored during that computation
/// since they are not allowed to be in a strongly connected component with
/// internal deps.
///
/// External deps are used to compute the dependent modules of each module once
/// the modules are decided.
///
/// Part files are also tracked here but ignored during computation of strongly
/// connected components, as they must always be a part of this assets module.
class _AssetNode {
final AssetId id;
/// The other internal sources that this file import or exports.
///
/// These may be merged into the same [Module] as this node, and are used when
/// computing strongly connected components.
final Set<AssetId> internalDeps;
/// Part files included by this asset.
///
/// These should always be a part of the same connected component.
final Set<AssetId> parts;
/// The deps of this source that are from an external package.
///
/// These are not used in computing strongly connected components (they are
/// not allowed to be in a strongly connected component with any of our
/// internal srcs).
final Set<AssetId> externalDeps;
/// Order in which this node was discovered.
int discoveryIndex;
/// Lowest discoveryIndex for any node this is connected to.
int lowestLinkedDiscoveryIndex;
_AssetNode(this.id, this.internalDeps, this.parts, this.externalDeps);
/// Creates an [_AssetNode] for [id] given a parsed [CompilationUnit] and some
/// [internalSrcs] which represent other assets that may become part of the
/// same module.
factory _AssetNode.forParsedUnit(
AssetId id, CompilationUnit parsed, Set<AssetId> internalSrcs) {
var externalDeps = new Set<AssetId>();
var internalDeps = new Set<AssetId>();
var parts = new Set<AssetId>();
for (var directive in parsed.directives) {
if (directive is! UriBasedDirective) continue;
var linkedId = importUriToAssetId(
id, (directive as UriBasedDirective).uri.stringValue);
if (linkedId == null) continue;
if (directive is PartDirective) {
if (!internalSrcs.contains(linkedId)) {
throw new StateError(
'Referenced part file $linkedId from $id which is not in the '
'same package');
}
parts.add(linkedId);
} else if (internalSrcs.contains(linkedId)) {
internalDeps.add(linkedId);
} else {
externalDeps.add(linkedId);
}
}
return new _AssetNode(id, internalDeps, parts, externalDeps);
}
}
/// Computes the ideal set of [Module]s for a group of [_AssetNode]s.
class _ModuleComputer {
final Set<AssetId> entrypoints;
final ModuleMode mode;
final Map<AssetId, _AssetNode> nodesById;
_ModuleComputer(this.entrypoints, this.mode, this.nodesById);
/// Does the actual computation of [Module]s.
///
/// See [computeModules] top level function for more information.
List<Module> _computeModules() {
var connectedComponents = _stronglyConnectedComponents();
var modulesById = _createModulesFromComponents(connectedComponents);
var modules = _mergeModules(modulesById);
return _renameSharedModules(modules);
}
/// Creates simple modules based strictly off of [connectedComponents].
///
/// This creates more modules than we want, but we collapse them later on.
Map<ModuleId, Module> _createModulesFromComponents(
Iterable<Set<_AssetNode>> connectedComponents) {
var modules = <ModuleId, Module>{};
for (var componentNodes in connectedComponents) {
// Name components based on first alphabetically sorted node, preferring
// public srcs (not under lib/src).
var sortedNodes = componentNodes.toList()
..sort((a, b) => a.id.path.compareTo(b.id.path));
var primaryNode = sortedNodes.firstWhere(
(node) => !node.id.path.startsWith('lib/src/'),
orElse: () => sortedNodes.first);
var moduleName =
p.url.split(p.withoutExtension(primaryNode.id.path)).join('__');
var id = new ModuleId(
primaryNode.id.package, moduleName, topLevelDir(primaryNode.id.path));
// Expand to include all the part files of each node, these aren't
// included as individual `_AssetNodes`s in `connectedComponents`.
var allAssetIds = componentNodes
.expand((node) => [node.id]..addAll(node.parts))
.toSet();
var allDepIds = new Set<AssetId>();
for (var node in componentNodes) {
allDepIds.addAll(node.externalDeps);
for (var id in node.internalDeps) {
if (allAssetIds.contains(id)) continue;
allDepIds.add(id);
}
}
var module = new Module(id, allAssetIds, allDepIds);
modules[module.id] = module;
}
return modules;
}
/// Filters [modules] to just those that contain entrypoint assets.
Set<ModuleId> _entryPointModules(Iterable<Module> modules) {
var entrypointModules = new Set<ModuleId>();
for (var module in modules) {
if (module.assetIds.intersection(entrypoints).isNotEmpty) {
entrypointModules.add(module.id);
}
}
return entrypointModules;
}
/// Creates a map of modules to the entrypoint modules that transitively
/// depend on those modules.
Map<ModuleId, Set<ModuleId>> _findReverseEntrypointDeps(
Set<ModuleId> entrypointModules, Map<ModuleId, Module> modulesById) {
var reverseDeps = <ModuleId, Set<ModuleId>>{};
var assetsToModules = <AssetId, Module>{};
for (var module in modulesById.values) {
for (var assetId in module.assetIds) {
assetsToModules[assetId] = module;
}
}
for (var id in entrypointModules) {
for (var moduleDep
in _localTransitiveDeps(modulesById[id], assetsToModules)) {
reverseDeps.putIfAbsent(moduleDep, () => new Set<ModuleId>()).add(id);
}
}
return reverseDeps;
}
/// Gets the local (same top level dir of the same package) transitive deps of
/// [module] using [assetsToModules].
Set<ModuleId> _localTransitiveDeps(
Module module, Map<AssetId, Module> assetsToModules) {
var localTransitiveDeps = new Set<ModuleId>();
var nextIds = module.directDependencies;
var seenIds = new Set<AssetId>();
while (nextIds.isNotEmpty) {
var ids = nextIds;
seenIds.addAll(ids);
nextIds = new Set<AssetId>();
for (var id in ids) {
var module = assetsToModules[id];
if (module == null) continue; // Skip non-local modules
if (localTransitiveDeps.add(module.id)) {
nextIds.addAll(module.directDependencies.difference(seenIds));
}
}
}
return localTransitiveDeps;
}
/// Merges [originalModulesById] into a minimum set of [Module]s using the
/// following rules:
///
/// * If it is an entrypoint module, skip it.
/// * Else merge it into a module whose name is a combination of all the
/// entrypoints that import it (create that module if it doesn't exist).
List<Module> _mergeModules(Map<ModuleId, Module> originalModulesById) {
var modulesById = new Map<ModuleId, Module>.from(originalModulesById);
// Maps modules to entrypoint modules that transitively depend on them.
var entrypointModuleIds = _entryPointModules(modulesById.values);
var modulesToEntryPoints =
_findReverseEntrypointDeps(entrypointModuleIds, modulesById);
for (var moduleId in modulesById.keys.toList()) {
// Skip entrypoint modules.
if (entrypointModuleIds.any((id) => id == moduleId)) continue;
// The entry points that transitively import this module.
var entrypointIds = modulesToEntryPoints[moduleId];
// If no entrypoint imports the module, just leave it alone.
if (entrypointIds == null || entrypointIds.isEmpty) continue;
// Create a new module based off the name of all entrypoints or merge into
// an existing one by that name.
var moduleNames = entrypointIds.map((id) => id.name).toList()..sort();
var newModuleId = new ModuleId(entrypointIds.first.package,
moduleNames.join('\$'), entrypointIds.first.dir);
var newModule = modulesById.putIfAbsent(
newModuleId,
() =>
new Module(newModuleId, new Set<AssetId>(), new Set<AssetId>()));
var oldModule = modulesById.remove(moduleId);
// Add all the original assets and deps to the new module.
newModule.assetIds.addAll(oldModule.assetIds);
newModule.directDependencies.addAll(oldModule.directDependencies);
// Clean up deps to remove assetIds, they may have been merged in.
newModule.directDependencies.removeAll(newModule.assetIds);
}
return modulesById.values.toList();
}
/// Renames shared [Module]s to something unique and short to avoid issues
/// with file names that are too long.
List<Module> _renameSharedModules(List<Module> modules) {
if (modules.isEmpty) return modules;
var next = 0;
return modules.map((module) {
if (module.id.name.contains('\$')) {
return new Module(
new ModuleId(module.id.package,
'${module.id.dir}__shared_${next++}', module.id.dir),
module.assetIds,
module.directDependencies);
} else {
return module;
}
}).toList();
}
/// Computes the strongly connected components reachable from [entrypoints].
List<Set<_AssetNode>> _stronglyConnectedComponents() {
var currentDiscoveryIndex = 0;
// [LinkedHashSet] maintains insertion order which is important!
var nodeStack = new LinkedHashSet<_AssetNode>();
var connectedComponents = <Set<_AssetNode>>[];
void stronglyConnect(_AssetNode node) {
node.discoveryIndex = currentDiscoveryIndex;
node.lowestLinkedDiscoveryIndex = currentDiscoveryIndex;
currentDiscoveryIndex++;
nodeStack.add(node);
for (var dep in node.internalDeps) {
var depNode = nodesById[dep];
if (depNode.discoveryIndex == null) {
stronglyConnect(depNode);
node.lowestLinkedDiscoveryIndex = min(node.lowestLinkedDiscoveryIndex,
depNode.lowestLinkedDiscoveryIndex);
} else if (nodeStack.contains(depNode)) {
node.lowestLinkedDiscoveryIndex = min(node.lowestLinkedDiscoveryIndex,
depNode.lowestLinkedDiscoveryIndex);
}
}
if (node.discoveryIndex == node.lowestLinkedDiscoveryIndex) {
var component = new Set<_AssetNode>();
// Pops the last node off of `nodeStack`, adds it to `component`, and
// returns it.
_AssetNode _popAndAddNode() {
var last = nodeStack.last;
nodeStack.remove(last);
component.add(last);
return last;
}
while (_popAndAddNode() != node) {}
connectedComponents.add(component);
}
}
for (var node in nodesById.values) {
if (node.discoveryIndex != null) continue;
stronglyConnect(node);
}
return connectedComponents;
}
}