-
Notifications
You must be signed in to change notification settings - Fork 224
/
module_computer.dart
423 lines (377 loc) · 15.3 KB
/
module_computer.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
// 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;
}
}