| // 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. |
| |
| import 'dart:collection' show HashSet, HashMap; |
| |
| import 'package:analyzer/analyzer.dart' show parseDirectives; |
| import 'package:analyzer/src/generated/ast.dart' |
| show |
| AstNode, |
| CompilationUnit, |
| ExportDirective, |
| Identifier, |
| ImportDirective, |
| LibraryDirective, |
| PartDirective, |
| PartOfDirective, |
| UriBasedDirective; |
| import 'package:analyzer/src/generated/engine.dart' show AnalysisContext; |
| import 'package:analyzer/src/task/dart.dart' show ParseDartTask; |
| import 'package:analyzer/src/generated/error.dart'; |
| import 'package:analyzer/src/generated/source.dart' show Source, SourceKind; |
| import 'package:html/dom.dart' show Document, Node, Element; |
| import 'package:html/parser.dart' as html; |
| import 'package:logging/logging.dart' show Logger, Level; |
| import 'package:path/path.dart' as path; |
| |
| import '../compiler.dart' show defaultRuntimeFiles; |
| import '../info.dart'; |
| import '../options.dart'; |
| import '../report.dart'; |
| import '../report/html_reporter.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 = {}; |
| |
| /// Resources included by default on any application. |
| final runtimeDeps = new Set<ResourceSourceNode>(); |
| |
| /// Analyzer used to resolve source files. |
| final AnalysisContext _context; |
| final AnalysisErrorListener _reporter; |
| final CompilerOptions _options; |
| |
| SourceGraph(this._context, this._reporter, this._options) { |
| var dir = _options.runtimeDir; |
| if (dir == null) { |
| _log.severe('Runtime dir could not be determined automatically, ' |
| 'please specify the --runtime-dir flag on the command line.'); |
| return; |
| } |
| var prefix = path.absolute(dir); |
| var files = _options.serverMode && _options.widget |
| ? runtimeFilesForServerMode |
| : defaultRuntimeFiles; |
| for (var file in files) { |
| runtimeDeps.add(nodeFromUri(path.toUri(path.join(prefix, file)))); |
| } |
| } |
| |
| /// Node associated with a resolved [uri]. |
| SourceNode nodeFromUri(Uri uri) { |
| var uriString = Uri.encodeFull('$uri'); |
| return nodes.putIfAbsent(uri, () { |
| var source = _context.sourceFactory.forUri(uriString); |
| var extension = path.extension(uriString); |
| if (extension == '.html') { |
| return new HtmlSourceNode(this, uri, source); |
| } else if (extension == '.dart' || uriString.startsWith('dart:')) { |
| return new DartSourceNode(this, uri, source); |
| } else { |
| return new ResourceSourceNode(this, uri, source); |
| } |
| }); |
| } |
| |
| List<String> get resources => _options.sourceOptions.resources; |
| } |
| |
| final runtimeFilesForServerMode = new List<String>.from(defaultRuntimeFiles) |
| ..add('messages_widget.js') |
| ..add('messages.css'); |
| |
| /// A node in the import graph representing a source file. |
| abstract class SourceNode { |
| final SourceGraph graph; |
| |
| /// 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. |
| Source _source; |
| Source get source => _source; |
| |
| String get contents => graph._context.getContents(_source).data; |
| |
| /// Last stamp read from `source.modificationStamp`. |
| /// This starts at -1, because analyzer uses that for files that don't exist. |
| int _lastStamp = -1; |
| |
| /// A hash used to help browsers cache the output that would be produced from |
| /// building this node. |
| String cachingHash; |
| |
| /// 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 in the [SourceGraph]. These include script tags for |
| /// [HtmlSourceNode]s; and imports, exports and parts for [DartSourceNode]s. |
| Iterable<SourceNode> get allDeps => const []; |
| |
| /// 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 => const []; |
| |
| SourceNode(this.graph, this.uri, this._source); |
| |
| /// Check for whether the file has changed and, if so, mark [needsRebuild] and |
| /// [structureChanged] as necessary. |
| void update() { |
| if (_source == null) { |
| _source = graph._context.sourceFactory.forUri(Uri.encodeFull('$uri')); |
| if (_source == null) return; |
| } |
| |
| int newStamp = _source.exists() ? _source.modificationStamp : -1; |
| if (newStamp > _lastStamp || newStamp == -1 && _lastStamp != -1) { |
| // If the timestamp changed, read the file from disk and cache it. |
| // We don't want the source text to change during compilation. |
| saveUpdatedContents(); |
| _lastStamp = newStamp; |
| needsRebuild = true; |
| } |
| } |
| |
| void clearSummary() {} |
| |
| void saveUpdatedContents() {} |
| |
| String toString() { |
| var simpleUri = uri.scheme == 'file' ? path.relative(uri.path) : "$uri"; |
| return '[$runtimeType: $simpleUri]'; |
| } |
| } |
| |
| /// A unique node representing all entry points in the graph. This is just for |
| /// graph algorthm convenience. |
| class EntryNode extends SourceNode { |
| final Iterable<SourceNode> entryPoints; |
| |
| @override |
| Iterable<SourceNode> get allDeps => entryPoints; |
| |
| @override |
| Iterable<SourceNode> get depsWithoutParts => entryPoints; |
| |
| EntryNode(SourceGraph graph, Uri uri, Iterable<SourceNode> nodes) |
| : entryPoints = nodes, |
| super(graph, uri, null); |
| } |
| |
| /// A node representing an entry HTML source file. |
| class HtmlSourceNode extends SourceNode { |
| /// Resources included by default on any application. |
| final runtimeDeps; |
| |
| /// Libraries referred to via script tags. |
| Set<DartSourceNode> scripts = new Set<DartSourceNode>(); |
| |
| /// Link-rel stylesheets, images, and other specified files. |
| Set<SourceNode> resources = new Set<SourceNode>(); |
| |
| @override |
| Iterable<SourceNode> get allDeps => |
| [scripts, resources, runtimeDeps].expand((e) => e); |
| |
| @override |
| Iterable<SourceNode> get depsWithoutParts => allDeps; |
| |
| /// Parsed document, updated whenever [update] is invoked. |
| Document document; |
| |
| /// Tracks resource files referenced from HTML nodes, e.g. |
| /// `<link rel=stylesheet href=...>` and `<img src=...>` |
| final htmlResourceNodes = new HashMap<Element, ResourceSourceNode>(); |
| |
| HtmlSourceNode(SourceGraph graph, Uri uri, Source source) |
| : runtimeDeps = graph.runtimeDeps, |
| super(graph, uri, source); |
| |
| @override |
| void clearSummary() { |
| var reporter = graph._reporter; |
| if (reporter is HtmlReporter) { |
| reporter.reporter.clearHtml(uri); |
| } else if (reporter is SummaryReporter) { |
| reporter.clearHtml(uri); |
| } |
| } |
| |
| @override |
| void update() { |
| super.update(); |
| if (needsRebuild) { |
| document = html.parse(contents, generateSpans: true); |
| var newScripts = new Set<DartSourceNode>(); |
| var tags = document.querySelectorAll('script[type="application/dart"]'); |
| for (var script in tags) { |
| var src = script.attributes['src']; |
| if (src == null) { |
| _reportError( |
| graph, |
| 'inlined script tags not supported at this time ' |
| '(see https://github.com/dart-lang/dart-dev-compiler/issues/54).', |
| script); |
| continue; |
| } |
| DartSourceNode node = graph.nodeFromUri(uri.resolve(src)); |
| if (node == null || !node.source.exists()) { |
| _reportError(graph, 'Script file $src not found', script); |
| } |
| if (node != null) newScripts.add(node); |
| } |
| |
| if (!_same(newScripts, scripts)) { |
| structureChanged = true; |
| scripts = newScripts; |
| } |
| |
| // TODO(jmesserly): simplify the design here. Ideally we wouldn't need |
| // to track user-defined CSS, images, etc. Also we don't have a clear |
| // way to distinguish runtime injected resources, like messages.css, from |
| // user-defined files. |
| htmlResourceNodes.clear(); |
| var newResources = new Set<SourceNode>(); |
| for (var resource in graph.resources) { |
| newResources.add(graph.nodeFromUri(uri.resolve(resource))); |
| } |
| for (var tag in document.querySelectorAll('link[rel="stylesheet"]')) { |
| ResourceSourceNode res = |
| graph.nodeFromUri(uri.resolve(tag.attributes['href'])); |
| htmlResourceNodes[tag] = res; |
| newResources.add(res); |
| } |
| for (var tag in document.querySelectorAll('img[src]')) { |
| ResourceSourceNode res = |
| graph.nodeFromUri(uri.resolve(tag.attributes['src'])); |
| htmlResourceNodes[tag] = res; |
| newResources.add(res); |
| } |
| if (!_same(newResources, resources)) { |
| structureChanged = true; |
| resources = newResources; |
| } |
| } |
| } |
| |
| void _reportError(SourceGraph graph, String message, Node node) { |
| var span = node.sourceSpan; |
| |
| // TODO(jmesserly): should these be errors or warnings? |
| var errorCode = new HtmlWarningCode('dev_compiler.$runtimeType', message); |
| graph._reporter.onError( |
| new AnalysisError(_source, span.start.offset, span.length, errorCode)); |
| } |
| } |
| |
| /// 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>(); |
| |
| /// How many times this file is included as a part. |
| int includedAsPart = 0; |
| |
| DartSourceNode(graph, uri, source) : super(graph, uri, source); |
| |
| @override |
| Iterable<SourceNode> get allDeps => |
| [imports, exports, parts].expand((e) => e); |
| |
| @override |
| Iterable<SourceNode> get depsWithoutParts => |
| [imports, exports].expand((e) => e); |
| |
| LibraryInfo info; |
| |
| // TODO(jmesserly): it would be nice to not keep all sources in memory at |
| // once, but how else can we ensure a consistent view across a given |
| // compile? One different from dev_compiler vs analyzer is that our |
| // messages later in the compiler need the original source text to print |
| // spans. We also read source text ourselves to parse directives. |
| // But we could discard it after that point. |
| void saveUpdatedContents() { |
| graph._context.setContents(_source, _source.contents.data); |
| } |
| |
| @override |
| void clearSummary() { |
| var reporter = graph._reporter; |
| if (reporter is HtmlReporter) { |
| reporter.reporter.clearLibrary(uri); |
| } else if (reporter is SummaryReporter) { |
| reporter.clearLibrary(uri); |
| } |
| } |
| |
| @override |
| void update() { |
| super.update(); |
| |
| if (needsRebuild) { |
| // If the defining compilation-unit changed, the structure might have |
| // changed. |
| var unit = parseDirectives(contents, name: _source.fullName); |
| 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 directiveUri = (d as UriBasedDirective).uri; |
| |
| // `dart:core` and other similar URLs only contain a name, but it is |
| // meant to be a folder when resolving relative paths from it. |
| var targetUri = uri.scheme == 'dart' && uri.pathSegments.length == 1 |
| ? Uri.parse('$uri/').resolve(directiveUri.stringValue) |
| : uri.resolve(directiveUri.stringValue); |
| var target = |
| ParseDartTask.resolveDirective(graph._context, _source, d, null); |
| DartSourceNode node = graph.nodes.putIfAbsent( |
| targetUri, () => new DartSourceNode(graph, targetUri, target)); |
| //var node = graph.nodeFromUri(targetUri); |
| if (node._source == null || !node._source.exists()) { |
| _reportError(graph, 'File $targetUri not found', d); |
| } |
| |
| 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; |
| |
| // 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; |
| } |
| } |
| |
| // 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(); |
| if (p.needsRebuild) needsRebuild = true; |
| } |
| } |
| |
| void _reportError(SourceGraph graph, String message, AstNode node) { |
| graph._reporter.onError(new AnalysisError(_source, node.offset, node.length, |
| new CompileTimeErrorCode('dev_compiler.$runtimeType', message))); |
| } |
| } |
| |
| /// Represents a runtime resource from our compiler that is needed to run an |
| /// application. |
| class ResourceSourceNode extends SourceNode { |
| ResourceSourceNode(graph, uri, source) : super(graph, uri, source); |
| } |
| |
| /// 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) { |
| visitInPreOrder(start, (n) => n.update(), 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, |
| includeParts: true); |
| } |
| |
| /// 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 ([DartSourceNode]) or HTML files |
| /// ([HtmlSourceNode]), but skip part files. That is because those are |
| /// built as part of some library. |
| /// |
| /// * 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 |
| /// `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 [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 |
| /// `n` may need to be rebuilt as well. |
| rebuild(SourceNode start, bool build(SourceNode node)) { |
| refreshStructureAndMarks(start); |
| // 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 htmlNeedsRebuild = false; |
| |
| bool shouldBuildNode(SourceNode n) { |
| if (n.needsRebuild) return true; |
| if (n is HtmlSourceNode) return htmlNeedsRebuild; |
| if (n is ResourceSourceNode || n is EntryNode) return false; |
| return (n as DartSourceNode) |
| .imports |
| .any((i) => apiChangeDetected.contains(i)); |
| } |
| |
| visitInPostOrder(start, (n) { |
| if (n.structureChanged) htmlNeedsRebuild = true; |
| if (shouldBuildNode(n)) { |
| var oldHash = n.cachingHash; |
| if (build(n)) apiChangeDetected.add(n); |
| if (oldHash != n.cachingHash) htmlNeedsRebuild = true; |
| } 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 [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 [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); |
| |
| final _log = new Logger('dev_compiler.dependency_graph'); |