| Index: lib/src/dependency_graph.dart
|
| diff --git a/lib/src/dependency_graph.dart b/lib/src/dependency_graph.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..3f656df3e376feb3c040cc3ad8e8be658f0534ee
|
| --- /dev/null
|
| +++ b/lib/src/dependency_graph.dart
|
| @@ -0,0 +1,333 @@
|
| +// Copyright (c) 2015, 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.
|
| +
|
| +/// Tracks the shape of the import/export graph and dependencies between files.
|
| +library ddc.src.dependency_graph;
|
| +
|
| +import 'dart:collection' show HashSet;
|
| +
|
| +import 'package:analyzer/analyzer.dart' show parseDirectives;
|
| +import 'package:analyzer/src/generated/ast.dart'
|
| + show
|
| + LibraryDirective,
|
| + ImportDirective,
|
| + ExportDirective,
|
| + PartDirective,
|
| + CompilationUnit,
|
| + Identifier;
|
| +import 'package:analyzer/src/generated/engine.dart'
|
| + show ParseDartTask, AnalysisContext;
|
| +import 'package:analyzer/src/generated/source.dart' show Source, SourceKind;
|
| +import 'package:html5lib/dom.dart' show Document;
|
| +import 'package:html5lib/parser.dart' as html;
|
| +import 'package:logging/logging.dart' show Logger;
|
| +import 'package:path/path.dart' as path;
|
| +
|
| +import 'info.dart';
|
| +import 'options.dart';
|
| +import 'utils.dart';
|
| +
|
| +/// Holds references to all source nodes in the import graph. This is mainly
|
| +/// used as a level of indirection to ensure that each source has a canonical
|
| +/// representation.
|
| +class SourceGraph {
|
| + /// All nodes in the source graph. Used to get a canonical representation for
|
| + /// any node.
|
| + final Map<Uri, SourceNode> nodes = {};
|
| +
|
| + /// Analyzer used to resolve source files.
|
| + final AnalysisContext _context;
|
| + final CompilerOptions _options;
|
| +
|
| + SourceGraph(this._context, this._options);
|
| +
|
| + /// Node associated with a resolved [uri].
|
| + SourceNode nodeFromUri(Uri uri, [bool isPart = false]) {
|
| + 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) {
|
| + 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 {
|
| + assert(false); // unreachable
|
| + }
|
| + });
|
| + }
|
| +}
|
| +
|
| +/// A node in the import graph representing a source file.
|
| +abstract class SourceNode {
|
| + /// Resolved URI for this node.
|
| + final Uri uri;
|
| +
|
| + /// Resolved source from the analyzer. We let the analyzer internally track
|
| + /// for modifications to the source files.
|
| + final Source source;
|
| +
|
| + /// Last stamp read from `source.modificationStamp`.
|
| + int _lastStamp = 0;
|
| +
|
| + /// Whether we need to rebuild this source file.
|
| + bool needsRebuild = false;
|
| +
|
| + /// Whether the structure of dependencies from this node (scripts, imports,
|
| + /// 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;
|
| +
|
| + SourceNode(this.uri, this.source);
|
| +
|
| + /// Check for whether the file has changed and, if so, mark [needsRebuild] and
|
| + /// [structureChanged] as necessary.
|
| + void update(SourceGraph graph) {
|
| + int newStamp = source.modificationStamp;
|
| + if (newStamp > _lastStamp) {
|
| + _lastStamp = newStamp;
|
| + needsRebuild = true;
|
| + }
|
| + }
|
| +
|
| + String toString() {
|
| + var simpleUri = uri.scheme == 'file' ? path.relative(uri.path) : "$uri";
|
| + return '[$runtimeType: $simpleUri]';
|
| + }
|
| +}
|
| +
|
| +/// A node representing an entry HTML source file.
|
| +class HtmlSourceNode extends SourceNode {
|
| + /// Libraries referred to via script tags.
|
| + Set<LibrarySourceNode> scripts = new Set<LibrarySourceNode>();
|
| +
|
| + @override
|
| + Iterable<SourceNode> get directDeps => scripts;
|
| +
|
| + /// Parsed document, updated whenever [update] is invoked.
|
| + Document document;
|
| +
|
| + HtmlSourceNode(uri, source) : super(uri, source);
|
| +
|
| + void update(SourceGraph graph) {
|
| + super.update(graph);
|
| + if (needsRebuild) {
|
| + document = html.parse(source.contents.data, generateSpans: true);
|
| + var newScripts = new Set<LibrarySourceNode>();
|
| + var tags = document.querySelectorAll('script[type="application/dart"]');
|
| + for (var script in tags) {
|
| + var src = script.attributes['src'];
|
| + if (src == null) {
|
| + // TODO(sigmund): expose these as compile-time failures
|
| + _log.severe(script.sourceSpan.message(
|
| + 'inlined script tags not supported at this time '
|
| + '(see https://github.com/dart-lang/dart-dev-compiler/issues/54).',
|
| + color: graph._options.useColors ? colorOf('error') : false));
|
| + continue;
|
| + }
|
| + var node = graph.nodeFromUri(uri.resolve(src));
|
| + if (!node.source.exists()) {
|
| + _log.severe(script.sourceSpan.message('Script file $src not found',
|
| + color: graph._options.useColors ? colorOf('error') : false));
|
| + }
|
| + newScripts.add(node);
|
| + }
|
| +
|
| + if (!_same(newScripts, scripts)) {
|
| + structureChanged = true;
|
| + scripts = newScripts;
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +/// 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.
|
| +class LibrarySourceNode extends SourceNode {
|
| + LibrarySourceNode(uri, source) : super(uri, source);
|
| +
|
| + Set<LibrarySourceNode> imports = new Set<LibrarySourceNode>();
|
| + Set<LibrarySourceNode> exports = new Set<LibrarySourceNode>();
|
| + Set<PartSourceNode> parts = new Set<PartSourceNode>();
|
| +
|
| + Iterable<SourceNode> get directDeps =>
|
| + [imports, exports, parts].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>();
|
| + for (var d in unit.directives) {
|
| + 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);
|
| + if (!node.source.exists()) {
|
| + _log.severe(spanForNode(unit, source, d).message(
|
| + 'File $uri not found',
|
| + color: graph._options.useColors ? colorOf('error') : false));
|
| + }
|
| +
|
| + if (d is ImportDirective) {
|
| + newImports.add(node);
|
| + } else if (d is ExportDirective) {
|
| + newExports.add(node);
|
| + } else if (d is PartDirective) {
|
| + newParts.add(node);
|
| + }
|
| + }
|
| +
|
| + if (!_same(newImports, imports)) {
|
| + structureChanged = true;
|
| + imports = newImports;
|
| + }
|
| +
|
| + if (!_same(newExports, exports)) {
|
| + structureChanged = true;
|
| + exports = newExports;
|
| + }
|
| +
|
| + if (!_same(newParts, parts)) {
|
| + structureChanged = true;
|
| + parts = newParts;
|
| + }
|
| + }
|
| +
|
| + // The library should be marked as needing rebuild if a part changed
|
| + // internally:
|
| + for (var p in parts) {
|
| + p.update(graph);
|
| + if (p.needsRebuild) needsRebuild = true;
|
| + }
|
| + }
|
| +}
|
| +
|
| +/// Updates the structure and `needsRebuild` marks in nodes of [graph] reachable
|
| +/// from [start].
|
| +///
|
| +/// That is, staring from [start], we update the graph by detecting file changes
|
| +/// and rebuilding the structure of the graph wherever it changed (an import was
|
| +/// added or removed, etc).
|
| +///
|
| +/// After calling this function a node is marked with `needsRebuild` only if it
|
| +/// contained local changes. Rebuild decisions that derive from transitive
|
| +/// 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));
|
| +}
|
| +
|
| +/// Clears all the `needsRebuild` and `structureChanged` marks in nodes
|
| +/// reachable from [start].
|
| +void clearMarks(SourceNode start) {
|
| + visitInPreOrder(start, (n) => n.needsRebuild = n.structureChanged = false);
|
| +}
|
| +
|
| +/// Traverses from [start] with the purpose of building any source that needs to
|
| +/// be rebuilt.
|
| +///
|
| +/// This function will call [build] in a post-order fashion, on a subset of the
|
| +/// 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.
|
| +///
|
| +/// * Always rebuild [LibrarySourceNode]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
|
| +/// `needsRebuild` bit.
|
| +///
|
| +/// * Rebuild [HtmlSourceNode]s if there were structural changes somewhere
|
| +/// 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
|
| +/// 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
|
| +/// `n` may need to be rebuilt as well.
|
| +rebuild(SourceNode start, SourceGraph graph, bool build(SourceNode node)) {
|
| + refreshStructureAndMarks(start, graph);
|
| + // Hold which source nodes may have changed their public API, this includes
|
| + // libraries that were modified or libraries that export other modified APIs.
|
| + // TODO(sigmund): consider removing this special support for exports? Many
|
| + // cases anways require using summaries to understand what parts of the public
|
| + // API may be affected by transitive changes. The re-export case is just one
|
| + // of those transitive cases, but is not sufficient. See
|
| + // https://github.com/dart-lang/dev_compiler/issues/76
|
| + var apiChangeDetected = new HashSet<SourceNode>();
|
| + 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
|
| + .any((i) => apiChangeDetected.contains(i));
|
| + }
|
| +
|
| + visitInPostOrder(start, (n) {
|
| + if (n.structureChanged) structureHasChanged = true;
|
| + if (shouldBuildNode(n)) {
|
| + if (build(n)) apiChangeDetected.add(n);
|
| + } else if (n is LibrarySourceNode &&
|
| + n.exports.any((e) => apiChangeDetected.contains(e))) {
|
| + apiChangeDetected.add(n);
|
| + }
|
| + n.needsRebuild = false;
|
| + n.structureChanged = 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 [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);
|
| +}
|
| +
|
| +bool _same(Set a, Set b) => a.length == b.length && a.containsAll(b);
|
| +final _log = new Logger('ddc.graph');
|
|
|