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