| Index: lib/src/dependency_graph.dart
|
| diff --git a/lib/src/dependency_graph.dart b/lib/src/dependency_graph.dart
|
| index 1092d6255dafc3a747ec7432206600851581eb7c..115525334b03b71f31fba36863c1f7288b28f83c 100644
|
| --- a/lib/src/dependency_graph.dart
|
| +++ b/lib/src/dependency_graph.dart
|
| @@ -14,6 +14,7 @@ import 'package:analyzer/src/generated/ast.dart'
|
| ImportDirective,
|
| ExportDirective,
|
| PartDirective,
|
| + PartOfDirective,
|
| CompilationUnit,
|
| Identifier;
|
| import 'package:analyzer/src/generated/engine.dart'
|
| @@ -43,26 +44,15 @@ class SourceGraph {
|
| SourceGraph(this._context, this._options);
|
|
|
| /// Node associated with a resolved [uri].
|
| - SourceNode nodeFromUri(Uri uri, [bool isPart = false]) {
|
| + SourceNode nodeFromUri(Uri uri) {
|
| var uriString = Uri.encodeFull('$uri');
|
| - var kind = uriString.endsWith('.html')
|
| - ? SourceKind.HTML
|
| - : isPart ? SourceKind.PART : SourceKind.LIBRARY;
|
| - return nodeFor(uri, _context.sourceFactory.forUri(uriString), kind);
|
| - }
|
| -
|
| - /// Construct the node of the given [kind] with the given [uri] and [source].
|
| - SourceNode nodeFor(Uri uri, Source source, SourceKind kind) {
|
| - // TODO(sigmund): validate canonicalization?
|
| - // TODO(sigmund): add support for changing a file from one kind to another
|
| - // (e.g. converting a file from a part to a library).
|
| return nodes.putIfAbsent(uri, () {
|
| - if (kind == SourceKind.HTML) {
|
| + var source = _context.sourceFactory.forUri(uriString);
|
| + var extension = path.extension(uriString);
|
| + if (extension == '.html') {
|
| return new HtmlSourceNode(uri, source);
|
| - } else if (kind == SourceKind.LIBRARY) {
|
| - return new LibrarySourceNode(uri, source);
|
| - } else if (kind == SourceKind.PART) {
|
| - return new PartSourceNode(uri, source);
|
| + } else if (extension == '.dart' || uriString.startsWith('dart:')) {
|
| + return new DartSourceNode(uri, source);
|
| } else {
|
| assert(false); // unreachable
|
| }
|
| @@ -89,9 +79,14 @@ abstract class SourceNode {
|
| /// exports, or parts) changed after we reparsed its contents.
|
| bool structureChanged = false;
|
|
|
| - /// Direct dependencies (script tags for HtmlSourceNodes; imports, exports and
|
| - /// parts for LibrarySourceNodes).
|
| - Iterable<SourceNode> get directDeps;
|
| + /// Direct dependencies in the [SourceGraph]. These include script tags for
|
| + /// [HtmlSourceNode]s; and imports, exports and parts for [DartSourceNode]s.
|
| + Iterable<SourceNode> get allDeps;
|
| +
|
| + /// Like [allDeps] but excludes parts for [DartSourceNode]s. For many
|
| + /// operations we mainly care about dependencies at the library level, so
|
| + /// parts are excluded from this list.
|
| + Iterable<SourceNode> get depsWithoutParts;
|
|
|
| SourceNode(this.uri, this.source);
|
|
|
| @@ -114,10 +109,13 @@ abstract class SourceNode {
|
| /// A node representing an entry HTML source file.
|
| class HtmlSourceNode extends SourceNode {
|
| /// Libraries referred to via script tags.
|
| - Set<LibrarySourceNode> scripts = new Set<LibrarySourceNode>();
|
| + Set<DartSourceNode> scripts = new Set<DartSourceNode>();
|
| +
|
| + @override
|
| + Iterable<SourceNode> get allDeps => scripts;
|
|
|
| @override
|
| - Iterable<SourceNode> get directDeps => scripts;
|
| + Iterable<SourceNode> get depsWithoutParts => scripts;
|
|
|
| /// Parsed document, updated whenever [update] is invoked.
|
| Document document;
|
| @@ -128,7 +126,7 @@ class HtmlSourceNode extends SourceNode {
|
| super.update(graph);
|
| if (needsRebuild) {
|
| document = html.parse(source.contents.data, generateSpans: true);
|
| - var newScripts = new Set<LibrarySourceNode>();
|
| + var newScripts = new Set<DartSourceNode>();
|
| var tags = document.querySelectorAll('script[type="application/dart"]');
|
| for (var script in tags) {
|
| var src = script.attributes['src'];
|
| @@ -156,41 +154,51 @@ class HtmlSourceNode extends SourceNode {
|
| }
|
| }
|
|
|
| -/// A node representing a Dart part.
|
| -class PartSourceNode extends SourceNode {
|
| - final Iterable<SourceNode> directDeps = const [];
|
| - PartSourceNode(uri, source) : super(uri, source);
|
| -}
|
| +/// A node representing a Dart library or part.
|
| +class DartSourceNode extends SourceNode {
|
| + /// Set of imported libraries (empty for part files).
|
| + Set<DartSourceNode> imports = new Set<DartSourceNode>();
|
| +
|
| + /// Set of exported libraries (empty for part files).
|
| + Set<DartSourceNode> exports = new Set<DartSourceNode>();
|
| +
|
| + /// Parts of this library (empty for part files).
|
| + Set<DartSourceNode> parts = new Set<DartSourceNode>();
|
|
|
| -/// A node representing a Dart library.
|
| -class LibrarySourceNode extends SourceNode {
|
| - LibrarySourceNode(uri, source) : super(uri, source);
|
| + /// How many times this file is included as a part.
|
| + int includedAsPart = 0;
|
|
|
| - Set<LibrarySourceNode> imports = new Set<LibrarySourceNode>();
|
| - Set<LibrarySourceNode> exports = new Set<LibrarySourceNode>();
|
| - Set<PartSourceNode> parts = new Set<PartSourceNode>();
|
| + DartSourceNode(uri, source) : super(uri, source);
|
|
|
| - Iterable<SourceNode> get directDeps =>
|
| + @override
|
| + Iterable<SourceNode> get allDeps =>
|
| [imports, exports, parts].expand((e) => e);
|
|
|
| + @override
|
| + Iterable<SourceNode> get depsWithoutParts =>
|
| + [imports, exports].expand((e) => e);
|
| +
|
| LibraryInfo info;
|
|
|
| void update(SourceGraph graph) {
|
| super.update(graph);
|
| +
|
| if (needsRebuild && source.contents.data != null) {
|
| // If the defining compilation-unit changed, the structure might have
|
| // changed.
|
| var unit = parseDirectives(source.contents.data, name: source.fullName);
|
| - var newImports = new Set<LibrarySourceNode>();
|
| - var newExports = new Set<LibrarySourceNode>();
|
| - var newParts = new Set<PartSourceNode>();
|
| + var newImports = new Set<DartSourceNode>();
|
| + var newExports = new Set<DartSourceNode>();
|
| + var newParts = new Set<DartSourceNode>();
|
| for (var d in unit.directives) {
|
| + // Nothing to do for parts.
|
| + if (d is PartOfDirective) return;
|
| if (d is LibraryDirective) continue;
|
| var target =
|
| ParseDartTask.resolveDirective(graph._context, source, d, null);
|
| var uri = target.uri;
|
| - var node = graph.nodeFor(uri, target,
|
| - d is PartDirective ? SourceKind.PART : SourceKind.LIBRARY);
|
| + var node =
|
| + graph.nodes.putIfAbsent(uri, () => new DartSourceNode(uri, target));
|
| if (!node.source.exists()) {
|
| _log.severe(spanForNode(unit, source, d).message(
|
| 'File $uri not found',
|
| @@ -218,6 +226,20 @@ class LibrarySourceNode extends SourceNode {
|
|
|
| if (!_same(newParts, parts)) {
|
| structureChanged = true;
|
| +
|
| + // When parts are removed, it's possible they were updated to be
|
| + // imported as a library
|
| + for (var p in parts) {
|
| + if (newParts.contains(p)) continue;
|
| + if (--p.includedAsPart == 0) {
|
| + p.needsRebuild = true;
|
| + }
|
| + }
|
| +
|
| + for (var p in newParts) {
|
| + if (parts.contains(p)) continue;
|
| + p.includedAsPart++;
|
| + }
|
| parts = newParts;
|
| }
|
| }
|
| @@ -225,6 +247,10 @@ class LibrarySourceNode extends SourceNode {
|
| // The library should be marked as needing rebuild if a part changed
|
| // internally:
|
| for (var p in parts) {
|
| + // Technically for parts we don't need to look at the contents. If they
|
| + // contain imports, exports, or parts, we'll ignore them in our crawling.
|
| + // However we do a full update to make it easier to adjust when users
|
| + // switch a file from a part to a library.
|
| p.update(graph);
|
| if (p.needsRebuild) needsRebuild = true;
|
| }
|
| @@ -243,13 +269,14 @@ class LibrarySourceNode extends SourceNode {
|
| /// changes (e.g. when the API of a dependency changed) are handled later in
|
| /// [rebuild].
|
| void refreshStructureAndMarks(SourceNode start, SourceGraph graph) {
|
| - visitInPreOrder(start, (n) => n.update(graph));
|
| + visitInPreOrder(start, (n) => n.update(graph), includeParts: false);
|
| }
|
|
|
| /// Clears all the `needsRebuild` and `structureChanged` marks in nodes
|
| /// reachable from [start].
|
| void clearMarks(SourceNode start) {
|
| - visitInPreOrder(start, (n) => n.needsRebuild = n.structureChanged = false);
|
| + visitInPreOrder(start, (n) => n.needsRebuild = n.structureChanged = false,
|
| + includeParts: true);
|
| }
|
|
|
| /// Traverses from [start] with the purpose of building any source that needs to
|
| @@ -259,11 +286,11 @@ void clearMarks(SourceNode start) {
|
| /// reachable nodes. There are four rules used to decide when to rebuild a node
|
| /// (call [build] on a node):
|
| ///
|
| -/// * Only rebuild Dart libraries ([LibrarySourceNode]) or HTML files
|
| -/// ([HtmlSourceNode]), but never part files ([PartSourceNode]). That is
|
| -/// because those are built as part of some library.
|
| +/// * Only rebuild Dart libraries ([DartSourceNode]) or HTML files
|
| +/// ([HtmlSourceNode]), but skip part files. That is because those are
|
| +/// built as part of some library.
|
| ///
|
| -/// * Always rebuild [LibrarySourceNode]s and [HtmlSourceNode]s with local
|
| +/// * Always rebuild [DartSourceNode]s and [HtmlSourceNode]s with local
|
| /// changes or changes in a part of the library. Internally this function
|
| /// calls [refreshStructureAndMarks] to ensure that the graph structure is
|
| /// up-to-date and that these nodes with local changes contain the
|
| @@ -273,7 +300,7 @@ void clearMarks(SourceNode start) {
|
| /// down its reachable subgraph. This is done because HTML files embed the
|
| /// transitive closure of the import graph in their output.
|
| ///
|
| -/// * Rebuild [LibrarySourceNode]s that depend on other [LibrarySourceNode]s
|
| +/// * Rebuild [DartSourceNode]s that depend on other [DartSourceNode]s
|
| /// whose API may have changed. The result of [build] is used to determine
|
| /// whether other nodes need to be rebuilt. The function [build] is expected
|
| /// to return `true` on a node `n` if it detemines other nodes that import
|
| @@ -291,10 +318,9 @@ rebuild(SourceNode start, SourceGraph graph, bool build(SourceNode node)) {
|
| bool structureHasChanged = false;
|
|
|
| bool shouldBuildNode(SourceNode n) {
|
| - if (n is PartSourceNode) return false;
|
| if (n.needsRebuild) return true;
|
| if (n is HtmlSourceNode) return structureHasChanged;
|
| - return (n as LibrarySourceNode).imports
|
| + return (n as DartSourceNode).imports
|
| .any((i) => apiChangeDetected.contains(i));
|
| }
|
|
|
| @@ -302,31 +328,48 @@ rebuild(SourceNode start, SourceGraph graph, bool build(SourceNode node)) {
|
| if (n.structureChanged) structureHasChanged = true;
|
| if (shouldBuildNode(n)) {
|
| if (build(n)) apiChangeDetected.add(n);
|
| - } else if (n is LibrarySourceNode &&
|
| + } else if (n is DartSourceNode &&
|
| n.exports.any((e) => apiChangeDetected.contains(e))) {
|
| apiChangeDetected.add(n);
|
| }
|
| n.needsRebuild = false;
|
| n.structureChanged = false;
|
| - });
|
| + if (n is DartSourceNode) {
|
| + // Note: clearing out flags in the parts could be a problem if someone
|
| + // tries to use a file both as a part and a library at the same time.
|
| + // In that case, we might not correctly propagate changes in the places
|
| + // where it is used as a library. Technically it's not allowed to have a
|
| + // file as a part and a library at once, and the analyzer should report an
|
| + // error in that case.
|
| + n.parts.forEach((p) => p.needsRebuild = p.structureChanged = false);
|
| + }
|
| + }, includeParts: false);
|
| }
|
|
|
| -/// Helper that runs [action] on nodes reachable from [node] in pre-order.
|
| -visitInPreOrder(SourceNode node, void action(SourceNode node),
|
| - {Set<SourceNode> seen}) {
|
| - if (seen == null) seen = new HashSet<SourceNode>();
|
| - if (!seen.add(node)) return;
|
| - action(node);
|
| - node.directDeps.forEach((d) => visitInPreOrder(d, action, seen: seen));
|
| +/// Helper that runs [action] on nodes reachable from [start] in pre-order.
|
| +visitInPreOrder(SourceNode start, void action(SourceNode node),
|
| + {bool includeParts: false}) {
|
| + var seen = new HashSet<SourceNode>();
|
| + helper(SourceNode node) {
|
| + if (!seen.add(node)) return;
|
| + action(node);
|
| + var deps = includeParts ? node.allDeps : node.depsWithoutParts;
|
| + deps.forEach(helper);
|
| + }
|
| + helper(start);
|
| }
|
|
|
| -/// Helper that runs [action] on nodes reachable from [node] in post-order.
|
| -visitInPostOrder(SourceNode node, void action(SourceNode node),
|
| - {Set<SourceNode> seen}) {
|
| - if (seen == null) seen = new HashSet<SourceNode>();
|
| - if (!seen.add(node)) return;
|
| - node.directDeps.forEach((d) => visitInPostOrder(d, action, seen: seen));
|
| - action(node);
|
| +/// Helper that runs [action] on nodes reachable from [start] in post-order.
|
| +visitInPostOrder(SourceNode start, void action(SourceNode node),
|
| + {bool includeParts: false}) {
|
| + var seen = new HashSet<SourceNode>();
|
| + helper(SourceNode node) {
|
| + if (!seen.add(node)) return;
|
| + var deps = includeParts ? node.allDeps : node.depsWithoutParts;
|
| + deps.forEach(helper);
|
| + action(node);
|
| + }
|
| + helper(start);
|
| }
|
|
|
| bool _same(Set a, Set b) => a.length == b.length && a.containsAll(b);
|
|
|