OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// Tracks the shape of the import/export graph and dependencies between files. |
| 6 library ddc.src.dependency_graph; |
| 7 |
| 8 import 'dart:collection' show HashSet; |
| 9 |
| 10 import 'package:analyzer/analyzer.dart' show parseDirectives; |
| 11 import 'package:analyzer/src/generated/ast.dart' |
| 12 show |
| 13 LibraryDirective, |
| 14 ImportDirective, |
| 15 ExportDirective, |
| 16 PartDirective, |
| 17 CompilationUnit, |
| 18 Identifier; |
| 19 import 'package:analyzer/src/generated/engine.dart' |
| 20 show ParseDartTask, AnalysisContext; |
| 21 import 'package:analyzer/src/generated/source.dart' show Source, SourceKind; |
| 22 import 'package:html5lib/dom.dart' show Document; |
| 23 import 'package:html5lib/parser.dart' as html; |
| 24 import 'package:logging/logging.dart' show Logger; |
| 25 import 'package:path/path.dart' as path; |
| 26 |
| 27 import 'info.dart'; |
| 28 import 'options.dart'; |
| 29 import 'utils.dart'; |
| 30 |
| 31 /// Holds references to all source nodes in the import graph. This is mainly |
| 32 /// used as a level of indirection to ensure that each source has a canonical |
| 33 /// representation. |
| 34 class SourceGraph { |
| 35 /// All nodes in the source graph. Used to get a canonical representation for |
| 36 /// any node. |
| 37 final Map<Uri, SourceNode> nodes = {}; |
| 38 |
| 39 /// Analyzer used to resolve source files. |
| 40 final AnalysisContext _context; |
| 41 final CompilerOptions _options; |
| 42 |
| 43 SourceGraph(this._context, this._options); |
| 44 |
| 45 /// Node associated with a resolved [uri]. |
| 46 SourceNode nodeFromUri(Uri uri, [bool isPart = false]) { |
| 47 var uriString = Uri.encodeFull('$uri'); |
| 48 var kind = uriString.endsWith('.html') |
| 49 ? SourceKind.HTML |
| 50 : isPart ? SourceKind.PART : SourceKind.LIBRARY; |
| 51 return nodeFor(uri, _context.sourceFactory.forUri(uriString), kind); |
| 52 } |
| 53 |
| 54 /// Construct the node of the given [kind] with the given [uri] and [source]. |
| 55 SourceNode nodeFor(Uri uri, Source source, SourceKind kind) { |
| 56 // TODO(sigmund): validate canonicalization? |
| 57 // TODO(sigmund): add support for changing a file from one kind to another |
| 58 // (e.g. converting a file from a part to a library). |
| 59 return nodes.putIfAbsent(uri, () { |
| 60 if (kind == SourceKind.HTML) { |
| 61 return new HtmlSourceNode(uri, source); |
| 62 } else if (kind == SourceKind.LIBRARY) { |
| 63 return new LibrarySourceNode(uri, source); |
| 64 } else if (kind == SourceKind.PART) { |
| 65 return new PartSourceNode(uri, source); |
| 66 } else { |
| 67 assert(false); // unreachable |
| 68 } |
| 69 }); |
| 70 } |
| 71 } |
| 72 |
| 73 /// A node in the import graph representing a source file. |
| 74 abstract class SourceNode { |
| 75 /// Resolved URI for this node. |
| 76 final Uri uri; |
| 77 |
| 78 /// Resolved source from the analyzer. We let the analyzer internally track |
| 79 /// for modifications to the source files. |
| 80 final Source source; |
| 81 |
| 82 /// Last stamp read from `source.modificationStamp`. |
| 83 int _lastStamp = 0; |
| 84 |
| 85 /// Whether we need to rebuild this source file. |
| 86 bool needsRebuild = false; |
| 87 |
| 88 /// Whether the structure of dependencies from this node (scripts, imports, |
| 89 /// exports, or parts) changed after we reparsed its contents. |
| 90 bool structureChanged = false; |
| 91 |
| 92 /// Direct dependencies (script tags for HtmlSourceNodes; imports, exports and |
| 93 /// parts for LibrarySourceNodes). |
| 94 Iterable<SourceNode> get directDeps; |
| 95 |
| 96 SourceNode(this.uri, this.source); |
| 97 |
| 98 /// Check for whether the file has changed and, if so, mark [needsRebuild] and |
| 99 /// [structureChanged] as necessary. |
| 100 void update(SourceGraph graph) { |
| 101 int newStamp = source.modificationStamp; |
| 102 if (newStamp > _lastStamp) { |
| 103 _lastStamp = newStamp; |
| 104 needsRebuild = true; |
| 105 } |
| 106 } |
| 107 |
| 108 String toString() { |
| 109 var simpleUri = uri.scheme == 'file' ? path.relative(uri.path) : "$uri"; |
| 110 return '[$runtimeType: $simpleUri]'; |
| 111 } |
| 112 } |
| 113 |
| 114 /// A node representing an entry HTML source file. |
| 115 class HtmlSourceNode extends SourceNode { |
| 116 /// Libraries referred to via script tags. |
| 117 Set<LibrarySourceNode> scripts = new Set<LibrarySourceNode>(); |
| 118 |
| 119 @override |
| 120 Iterable<SourceNode> get directDeps => scripts; |
| 121 |
| 122 /// Parsed document, updated whenever [update] is invoked. |
| 123 Document document; |
| 124 |
| 125 HtmlSourceNode(uri, source) : super(uri, source); |
| 126 |
| 127 void update(SourceGraph graph) { |
| 128 super.update(graph); |
| 129 if (needsRebuild) { |
| 130 document = html.parse(source.contents.data, generateSpans: true); |
| 131 var newScripts = new Set<LibrarySourceNode>(); |
| 132 var tags = document.querySelectorAll('script[type="application/dart"]'); |
| 133 for (var script in tags) { |
| 134 var src = script.attributes['src']; |
| 135 if (src == null) { |
| 136 // TODO(sigmund): expose these as compile-time failures |
| 137 _log.severe(script.sourceSpan.message( |
| 138 'inlined script tags not supported at this time ' |
| 139 '(see https://github.com/dart-lang/dart-dev-compiler/issues/54).', |
| 140 color: graph._options.useColors ? colorOf('error') : false)); |
| 141 continue; |
| 142 } |
| 143 var node = graph.nodeFromUri(uri.resolve(src)); |
| 144 if (!node.source.exists()) { |
| 145 _log.severe(script.sourceSpan.message('Script file $src not found', |
| 146 color: graph._options.useColors ? colorOf('error') : false)); |
| 147 } |
| 148 newScripts.add(node); |
| 149 } |
| 150 |
| 151 if (!_same(newScripts, scripts)) { |
| 152 structureChanged = true; |
| 153 scripts = newScripts; |
| 154 } |
| 155 } |
| 156 } |
| 157 } |
| 158 |
| 159 /// A node representing a Dart part. |
| 160 class PartSourceNode extends SourceNode { |
| 161 final Iterable<SourceNode> directDeps = const []; |
| 162 PartSourceNode(uri, source) : super(uri, source); |
| 163 } |
| 164 |
| 165 /// A node representing a Dart library. |
| 166 class LibrarySourceNode extends SourceNode { |
| 167 LibrarySourceNode(uri, source) : super(uri, source); |
| 168 |
| 169 Set<LibrarySourceNode> imports = new Set<LibrarySourceNode>(); |
| 170 Set<LibrarySourceNode> exports = new Set<LibrarySourceNode>(); |
| 171 Set<PartSourceNode> parts = new Set<PartSourceNode>(); |
| 172 |
| 173 Iterable<SourceNode> get directDeps => |
| 174 [imports, exports, parts].expand((e) => e); |
| 175 |
| 176 LibraryInfo info; |
| 177 |
| 178 void update(SourceGraph graph) { |
| 179 super.update(graph); |
| 180 if (needsRebuild && source.contents.data != null) { |
| 181 // If the defining compilation-unit changed, the structure might have |
| 182 // changed. |
| 183 var unit = parseDirectives(source.contents.data, name: source.fullName); |
| 184 var newImports = new Set<LibrarySourceNode>(); |
| 185 var newExports = new Set<LibrarySourceNode>(); |
| 186 var newParts = new Set<PartSourceNode>(); |
| 187 for (var d in unit.directives) { |
| 188 if (d is LibraryDirective) continue; |
| 189 var target = |
| 190 ParseDartTask.resolveDirective(graph._context, source, d, null); |
| 191 var uri = target.uri; |
| 192 var node = graph.nodeFor(uri, target, |
| 193 d is PartDirective ? SourceKind.PART : SourceKind.LIBRARY); |
| 194 if (!node.source.exists()) { |
| 195 _log.severe(spanForNode(unit, source, d).message( |
| 196 'File $uri not found', |
| 197 color: graph._options.useColors ? colorOf('error') : false)); |
| 198 } |
| 199 |
| 200 if (d is ImportDirective) { |
| 201 newImports.add(node); |
| 202 } else if (d is ExportDirective) { |
| 203 newExports.add(node); |
| 204 } else if (d is PartDirective) { |
| 205 newParts.add(node); |
| 206 } |
| 207 } |
| 208 |
| 209 if (!_same(newImports, imports)) { |
| 210 structureChanged = true; |
| 211 imports = newImports; |
| 212 } |
| 213 |
| 214 if (!_same(newExports, exports)) { |
| 215 structureChanged = true; |
| 216 exports = newExports; |
| 217 } |
| 218 |
| 219 if (!_same(newParts, parts)) { |
| 220 structureChanged = true; |
| 221 parts = newParts; |
| 222 } |
| 223 } |
| 224 |
| 225 // The library should be marked as needing rebuild if a part changed |
| 226 // internally: |
| 227 for (var p in parts) { |
| 228 p.update(graph); |
| 229 if (p.needsRebuild) needsRebuild = true; |
| 230 } |
| 231 } |
| 232 } |
| 233 |
| 234 /// Updates the structure and `needsRebuild` marks in nodes of [graph] reachable |
| 235 /// from [start]. |
| 236 /// |
| 237 /// That is, staring from [start], we update the graph by detecting file changes |
| 238 /// and rebuilding the structure of the graph wherever it changed (an import was |
| 239 /// added or removed, etc). |
| 240 /// |
| 241 /// After calling this function a node is marked with `needsRebuild` only if it |
| 242 /// contained local changes. Rebuild decisions that derive from transitive |
| 243 /// changes (e.g. when the API of a dependency changed) are handled later in |
| 244 /// [rebuild]. |
| 245 void refreshStructureAndMarks(SourceNode start, SourceGraph graph) { |
| 246 visitInPreOrder(start, (n) => n.update(graph)); |
| 247 } |
| 248 |
| 249 /// Clears all the `needsRebuild` and `structureChanged` marks in nodes |
| 250 /// reachable from [start]. |
| 251 void clearMarks(SourceNode start) { |
| 252 visitInPreOrder(start, (n) => n.needsRebuild = n.structureChanged = false); |
| 253 } |
| 254 |
| 255 /// Traverses from [start] with the purpose of building any source that needs to |
| 256 /// be rebuilt. |
| 257 /// |
| 258 /// This function will call [build] in a post-order fashion, on a subset of the |
| 259 /// reachable nodes. There are four rules used to decide when to rebuild a node |
| 260 /// (call [build] on a node): |
| 261 /// |
| 262 /// * Only rebuild Dart libraries ([LibrarySourceNode]) or HTML files |
| 263 /// ([HtmlSourceNode]), but never part files ([PartSourceNode]). That is |
| 264 /// because those are built as part of some library. |
| 265 /// |
| 266 /// * Always rebuild [LibrarySourceNode]s and [HtmlSourceNode]s with local |
| 267 /// changes or changes in a part of the library. Internally this function |
| 268 /// calls [refreshStructureAndMarks] to ensure that the graph structure is |
| 269 /// up-to-date and that these nodes with local changes contain the |
| 270 /// `needsRebuild` bit. |
| 271 /// |
| 272 /// * Rebuild [HtmlSourceNode]s if there were structural changes somewhere |
| 273 /// down its reachable subgraph. This is done because HTML files embed the |
| 274 /// transitive closure of the import graph in their output. |
| 275 /// |
| 276 /// * Rebuild [LibrarySourceNode]s that depend on other [LibrarySourceNode]s |
| 277 /// whose API may have changed. The result of [build] is used to determine |
| 278 /// whether other nodes need to be rebuilt. The function [build] is expected |
| 279 /// to return `true` on a node `n` if it detemines other nodes that import |
| 280 /// `n` may need to be rebuilt as well. |
| 281 rebuild(SourceNode start, SourceGraph graph, bool build(SourceNode node)) { |
| 282 refreshStructureAndMarks(start, graph); |
| 283 // Hold which source nodes may have changed their public API, this includes |
| 284 // libraries that were modified or libraries that export other modified APIs. |
| 285 // TODO(sigmund): consider removing this special support for exports? Many |
| 286 // cases anways require using summaries to understand what parts of the public |
| 287 // API may be affected by transitive changes. The re-export case is just one |
| 288 // of those transitive cases, but is not sufficient. See |
| 289 // https://github.com/dart-lang/dev_compiler/issues/76 |
| 290 var apiChangeDetected = new HashSet<SourceNode>(); |
| 291 bool structureHasChanged = false; |
| 292 |
| 293 bool shouldBuildNode(SourceNode n) { |
| 294 if (n is PartSourceNode) return false; |
| 295 if (n.needsRebuild) return true; |
| 296 if (n is HtmlSourceNode) return structureHasChanged; |
| 297 return (n as LibrarySourceNode).imports |
| 298 .any((i) => apiChangeDetected.contains(i)); |
| 299 } |
| 300 |
| 301 visitInPostOrder(start, (n) { |
| 302 if (n.structureChanged) structureHasChanged = true; |
| 303 if (shouldBuildNode(n)) { |
| 304 if (build(n)) apiChangeDetected.add(n); |
| 305 } else if (n is LibrarySourceNode && |
| 306 n.exports.any((e) => apiChangeDetected.contains(e))) { |
| 307 apiChangeDetected.add(n); |
| 308 } |
| 309 n.needsRebuild = false; |
| 310 n.structureChanged = false; |
| 311 }); |
| 312 } |
| 313 |
| 314 /// Helper that runs [action] on nodes reachable from [node] in pre-order. |
| 315 visitInPreOrder(SourceNode node, void action(SourceNode node), |
| 316 {Set<SourceNode> seen}) { |
| 317 if (seen == null) seen = new HashSet<SourceNode>(); |
| 318 if (!seen.add(node)) return; |
| 319 action(node); |
| 320 node.directDeps.forEach((d) => visitInPreOrder(d, action, seen: seen)); |
| 321 } |
| 322 |
| 323 /// Helper that runs [action] on nodes reachable from [node] in post-order. |
| 324 visitInPostOrder(SourceNode node, void action(SourceNode node), |
| 325 {Set<SourceNode> seen}) { |
| 326 if (seen == null) seen = new HashSet<SourceNode>(); |
| 327 if (!seen.add(node)) return; |
| 328 node.directDeps.forEach((d) => visitInPostOrder(d, action, seen: seen)); |
| 329 action(node); |
| 330 } |
| 331 |
| 332 bool _same(Set a, Set b) => a.length == b.length && a.containsAll(b); |
| 333 final _log = new Logger('ddc.graph'); |
OLD | NEW |