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 | |
7 import 'dart:collection' show HashSet, HashMap; | |
8 | |
9 import 'package:analyzer/analyzer.dart' show parseDirectives; | |
10 import 'package:analyzer/dart/ast/ast.dart' | |
11 show | |
12 AstNode, | |
13 CompilationUnit, | |
14 ExportDirective, | |
15 Identifier, | |
16 ImportDirective, | |
17 LibraryDirective, | |
18 PartDirective, | |
19 PartOfDirective, | |
20 UriBasedDirective; | |
21 import 'package:analyzer/src/generated/engine.dart' show AnalysisContext; | |
22 import 'package:analyzer/src/generated/error.dart'; | |
23 import 'package:analyzer/src/generated/source.dart' show Source, SourceKind; | |
24 import 'package:analyzer/src/task/dart.dart' show ParseDartTask; | |
25 import 'package:html/dom.dart' show Document, Node, Element; | |
26 import 'package:html/parser.dart' as html; | |
27 import 'package:logging/logging.dart' show Logger, Level; | |
28 import 'package:path/path.dart' as path; | |
29 | |
30 import '../compiler.dart' show defaultRuntimeFiles; | |
31 import '../info.dart'; | |
32 import '../options.dart'; | |
33 import '../report.dart'; | |
34 import '../report/html_reporter.dart'; | |
35 | |
36 /// Holds references to all source nodes in the import graph. This is mainly | |
37 /// used as a level of indirection to ensure that each source has a canonical | |
38 /// representation. | |
39 class SourceGraph { | |
40 /// All nodes in the source graph. Used to get a canonical representation for | |
41 /// any node. | |
42 final Map<Uri, SourceNode> nodes = {}; | |
43 | |
44 /// Resources included by default on any application. | |
45 final runtimeDeps = new Set<ResourceSourceNode>(); | |
46 | |
47 /// Analyzer used to resolve source files. | |
48 final AnalysisContext _context; | |
49 final AnalysisErrorListener _reporter; | |
50 final CompilerOptions _options; | |
51 | |
52 SourceGraph(this._context, this._reporter, this._options) { | |
53 var dir = _options.runtimeDir; | |
54 if (dir == null) { | |
55 _log.severe('Runtime dir could not be determined automatically, ' | |
56 'please specify the --runtime-dir flag on the command line.'); | |
57 return; | |
58 } | |
59 var prefix = path.absolute(dir); | |
60 var files = _options.serverMode && _options.widget | |
61 ? runtimeFilesForServerMode | |
62 : defaultRuntimeFiles; | |
63 for (var file in files) { | |
64 runtimeDeps.add(nodeFromUri(path.toUri(path.join(prefix, file)))); | |
65 } | |
66 } | |
67 | |
68 /// Node associated with a resolved [uri]. | |
69 SourceNode nodeFromUri(Uri uri) { | |
70 var uriString = Uri.encodeFull('$uri'); | |
71 return nodes.putIfAbsent(uri, () { | |
72 var source = _context.sourceFactory.forUri(uriString); | |
73 var extension = path.extension(uriString); | |
74 if (extension == '.html') { | |
75 return new HtmlSourceNode(this, uri, source); | |
76 } else if (extension == '.dart' || uriString.startsWith('dart:')) { | |
77 return new DartSourceNode(this, uri, source); | |
78 } else { | |
79 return new ResourceSourceNode(this, uri, source); | |
80 } | |
81 }); | |
82 } | |
83 | |
84 List<String> get resources => _options.sourceOptions.resources; | |
85 } | |
86 | |
87 final runtimeFilesForServerMode = new List<String>.from(defaultRuntimeFiles) | |
88 ..add('messages_widget.js') | |
89 ..add('messages.css'); | |
90 | |
91 /// A node in the import graph representing a source file. | |
92 abstract class SourceNode { | |
93 final SourceGraph graph; | |
94 | |
95 /// Resolved URI for this node. | |
96 final Uri uri; | |
97 | |
98 /// Resolved source from the analyzer. We let the analyzer internally track | |
99 /// for modifications to the source files. | |
100 Source _source; | |
101 Source get source => _source; | |
102 | |
103 String get contents => graph._context.getContents(_source).data; | |
104 | |
105 /// Last stamp read from `source.modificationStamp`. | |
106 /// This starts at -1, because analyzer uses that for files that don't exist. | |
107 int _lastStamp = -1; | |
108 | |
109 /// A hash used to help browsers cache the output that would be produced from | |
110 /// building this node. | |
111 String cachingHash; | |
112 | |
113 /// Whether we need to rebuild this source file. | |
114 bool needsRebuild = false; | |
115 | |
116 /// Whether the structure of dependencies from this node (scripts, imports, | |
117 /// exports, or parts) changed after we reparsed its contents. | |
118 bool structureChanged = false; | |
119 | |
120 /// Direct dependencies in the [SourceGraph]. These include script tags for | |
121 /// [HtmlSourceNode]s; and imports, exports and parts for [DartSourceNode]s. | |
122 Iterable<SourceNode> get allDeps => const []; | |
123 | |
124 /// Like [allDeps] but excludes parts for [DartSourceNode]s. For many | |
125 /// operations we mainly care about dependencies at the library level, so | |
126 /// parts are excluded from this list. | |
127 Iterable<SourceNode> get depsWithoutParts => const []; | |
128 | |
129 SourceNode(this.graph, this.uri, this._source); | |
130 | |
131 /// Check for whether the file has changed and, if so, mark [needsRebuild] and | |
132 /// [structureChanged] as necessary. | |
133 void update() { | |
134 if (_source == null) { | |
135 _source = graph._context.sourceFactory.forUri(Uri.encodeFull('$uri')); | |
136 if (_source == null) return; | |
137 } | |
138 | |
139 int newStamp = _source.exists() ? _source.modificationStamp : -1; | |
140 if (newStamp > _lastStamp || newStamp == -1 && _lastStamp != -1) { | |
141 // If the timestamp changed, read the file from disk and cache it. | |
142 // We don't want the source text to change during compilation. | |
143 saveUpdatedContents(); | |
144 _lastStamp = newStamp; | |
145 needsRebuild = true; | |
146 } | |
147 } | |
148 | |
149 void clearSummary() {} | |
150 | |
151 void saveUpdatedContents() {} | |
152 | |
153 String toString() { | |
154 var simpleUri = uri.scheme == 'file' ? path.relative(uri.path) : "$uri"; | |
155 return '[$runtimeType: $simpleUri]'; | |
156 } | |
157 } | |
158 | |
159 /// A unique node representing all entry points in the graph. This is just for | |
160 /// graph algorthm convenience. | |
161 class EntryNode extends SourceNode { | |
162 final Iterable<SourceNode> entryPoints; | |
163 | |
164 @override | |
165 Iterable<SourceNode> get allDeps => entryPoints; | |
166 | |
167 @override | |
168 Iterable<SourceNode> get depsWithoutParts => entryPoints; | |
169 | |
170 EntryNode(SourceGraph graph, Uri uri, Iterable<SourceNode> nodes) | |
171 : entryPoints = nodes, | |
172 super(graph, uri, null); | |
173 } | |
174 | |
175 /// A node representing an entry HTML source file. | |
176 class HtmlSourceNode extends SourceNode { | |
177 /// Resources included by default on any application. | |
178 final runtimeDeps; | |
179 | |
180 /// Libraries referred to via script tags. | |
181 Set<DartSourceNode> scripts = new Set<DartSourceNode>(); | |
182 | |
183 /// Link-rel stylesheets, images, and other specified files. | |
184 Set<SourceNode> resources = new Set<SourceNode>(); | |
185 | |
186 @override | |
187 Iterable<SourceNode> get allDeps => | |
188 [scripts, resources, runtimeDeps].expand((e) => e); | |
189 | |
190 @override | |
191 Iterable<SourceNode> get depsWithoutParts => allDeps; | |
192 | |
193 /// Parsed document, updated whenever [update] is invoked. | |
194 Document document; | |
195 | |
196 /// Tracks resource files referenced from HTML nodes, e.g. | |
197 /// `<link rel=stylesheet href=...>` and `<img src=...>` | |
198 final htmlResourceNodes = new HashMap<Element, ResourceSourceNode>(); | |
199 | |
200 HtmlSourceNode(SourceGraph graph, Uri uri, Source source) | |
201 : runtimeDeps = graph.runtimeDeps, | |
202 super(graph, uri, source); | |
203 | |
204 @override | |
205 void clearSummary() { | |
206 var reporter = graph._reporter; | |
207 if (reporter is HtmlReporter) { | |
208 reporter.reporter.clearHtml(uri); | |
209 } else if (reporter is SummaryReporter) { | |
210 reporter.clearHtml(uri); | |
211 } | |
212 } | |
213 | |
214 @override | |
215 void update() { | |
216 super.update(); | |
217 if (needsRebuild) { | |
218 document = html.parse(contents, generateSpans: true); | |
219 var newScripts = new Set<DartSourceNode>(); | |
220 var tags = document.querySelectorAll('script[type="application/dart"]'); | |
221 for (var script in tags) { | |
222 var src = script.attributes['src']; | |
223 if (src == null) { | |
224 _reportError( | |
225 graph, | |
226 'inlined script tags not supported at this time ' | |
227 '(see https://github.com/dart-lang/dart-dev-compiler/issues/54).', | |
228 script); | |
229 continue; | |
230 } | |
231 DartSourceNode node = graph.nodeFromUri(uri.resolve(src)); | |
232 if (node == null || !node.source.exists()) { | |
233 _reportError(graph, 'Script file $src not found', script); | |
234 } | |
235 if (node != null) newScripts.add(node); | |
236 } | |
237 | |
238 if (!_same(newScripts, scripts)) { | |
239 structureChanged = true; | |
240 scripts = newScripts; | |
241 } | |
242 | |
243 // TODO(jmesserly): simplify the design here. Ideally we wouldn't need | |
244 // to track user-defined CSS, images, etc. Also we don't have a clear | |
245 // way to distinguish runtime injected resources, like messages.css, from | |
246 // user-defined files. | |
247 htmlResourceNodes.clear(); | |
248 var newResources = new Set<SourceNode>(); | |
249 for (var resource in graph.resources) { | |
250 newResources.add(graph.nodeFromUri(uri.resolve(resource))); | |
251 } | |
252 for (var tag in document.querySelectorAll('link[rel="stylesheet"]')) { | |
253 ResourceSourceNode res = | |
254 graph.nodeFromUri(uri.resolve(tag.attributes['href'])); | |
255 htmlResourceNodes[tag] = res; | |
256 newResources.add(res); | |
257 } | |
258 for (var tag in document.querySelectorAll('img[src]')) { | |
259 ResourceSourceNode res = | |
260 graph.nodeFromUri(uri.resolve(tag.attributes['src'])); | |
261 htmlResourceNodes[tag] = res; | |
262 newResources.add(res); | |
263 } | |
264 if (!_same(newResources, resources)) { | |
265 structureChanged = true; | |
266 resources = newResources; | |
267 } | |
268 } | |
269 } | |
270 | |
271 void _reportError(SourceGraph graph, String message, Node node) { | |
272 var span = node.sourceSpan; | |
273 | |
274 // TODO(jmesserly): should these be errors or warnings? | |
275 var errorCode = new HtmlWarningCode('dev_compiler.$runtimeType', message); | |
276 graph._reporter.onError( | |
277 new AnalysisError(_source, span.start.offset, span.length, errorCode)); | |
278 } | |
279 } | |
280 | |
281 /// A node representing a Dart library or part. | |
282 class DartSourceNode extends SourceNode { | |
283 /// Set of imported libraries (empty for part files). | |
284 Set<DartSourceNode> imports = new Set<DartSourceNode>(); | |
285 | |
286 /// Set of exported libraries (empty for part files). | |
287 Set<DartSourceNode> exports = new Set<DartSourceNode>(); | |
288 | |
289 /// Parts of this library (empty for part files). | |
290 Set<DartSourceNode> parts = new Set<DartSourceNode>(); | |
291 | |
292 /// How many times this file is included as a part. | |
293 int includedAsPart = 0; | |
294 | |
295 DartSourceNode(graph, uri, source) : super(graph, uri, source); | |
296 | |
297 @override | |
298 Iterable<SourceNode> get allDeps => | |
299 [imports, exports, parts].expand((e) => e); | |
300 | |
301 @override | |
302 Iterable<SourceNode> get depsWithoutParts => | |
303 [imports, exports].expand((e) => e); | |
304 | |
305 LibraryInfo info; | |
306 | |
307 // TODO(jmesserly): it would be nice to not keep all sources in memory at | |
308 // once, but how else can we ensure a consistent view across a given | |
309 // compile? One different from dev_compiler vs analyzer is that our | |
310 // messages later in the compiler need the original source text to print | |
311 // spans. We also read source text ourselves to parse directives. | |
312 // But we could discard it after that point. | |
313 void saveUpdatedContents() { | |
314 graph._context.setContents(_source, _source.contents.data); | |
315 } | |
316 | |
317 @override | |
318 void clearSummary() { | |
319 var reporter = graph._reporter; | |
320 if (reporter is HtmlReporter) { | |
321 reporter.reporter.clearLibrary(uri); | |
322 } else if (reporter is SummaryReporter) { | |
323 reporter.clearLibrary(uri); | |
324 } | |
325 } | |
326 | |
327 @override | |
328 void update() { | |
329 super.update(); | |
330 | |
331 if (needsRebuild) { | |
332 // If the defining compilation-unit changed, the structure might have | |
333 // changed. | |
334 var unit = parseDirectives(contents, name: _source.fullName); | |
335 var newImports = new Set<DartSourceNode>(); | |
336 var newExports = new Set<DartSourceNode>(); | |
337 var newParts = new Set<DartSourceNode>(); | |
338 for (var d in unit.directives) { | |
339 // Nothing to do for parts. | |
340 if (d is PartOfDirective) return; | |
341 if (d is LibraryDirective) continue; | |
342 | |
343 var directiveUri = (d as UriBasedDirective).uri; | |
344 | |
345 // `dart:core` and other similar URLs only contain a name, but it is | |
346 // meant to be a folder when resolving relative paths from it. | |
347 var targetUri = uri.scheme == 'dart' && uri.pathSegments.length == 1 | |
348 ? Uri.parse('$uri/').resolve(directiveUri.stringValue) | |
349 : uri.resolve(directiveUri.stringValue); | |
350 var target = | |
351 ParseDartTask.resolveDirective(graph._context, _source, d, null); | |
352 DartSourceNode node = graph.nodes.putIfAbsent( | |
353 targetUri, () => new DartSourceNode(graph, targetUri, target)); | |
354 //var node = graph.nodeFromUri(targetUri); | |
355 if (node._source == null || !node._source.exists()) { | |
356 _reportError(graph, 'File $targetUri not found', d); | |
357 } | |
358 | |
359 if (d is ImportDirective) { | |
360 newImports.add(node); | |
361 } else if (d is ExportDirective) { | |
362 newExports.add(node); | |
363 } else if (d is PartDirective) { | |
364 newParts.add(node); | |
365 } | |
366 } | |
367 | |
368 if (!_same(newImports, imports)) { | |
369 structureChanged = true; | |
370 imports = newImports; | |
371 } | |
372 | |
373 if (!_same(newExports, exports)) { | |
374 structureChanged = true; | |
375 exports = newExports; | |
376 } | |
377 | |
378 if (!_same(newParts, parts)) { | |
379 structureChanged = true; | |
380 | |
381 // When parts are removed, it's possible they were updated to be | |
382 // imported as a library | |
383 for (var p in parts) { | |
384 if (newParts.contains(p)) continue; | |
385 if (--p.includedAsPart == 0) { | |
386 p.needsRebuild = true; | |
387 } | |
388 } | |
389 | |
390 for (var p in newParts) { | |
391 if (parts.contains(p)) continue; | |
392 p.includedAsPart++; | |
393 } | |
394 parts = newParts; | |
395 } | |
396 } | |
397 | |
398 // The library should be marked as needing rebuild if a part changed | |
399 // internally: | |
400 for (var p in parts) { | |
401 // Technically for parts we don't need to look at the contents. If they | |
402 // contain imports, exports, or parts, we'll ignore them in our crawling. | |
403 // However we do a full update to make it easier to adjust when users | |
404 // switch a file from a part to a library. | |
405 p.update(); | |
406 if (p.needsRebuild) needsRebuild = true; | |
407 } | |
408 } | |
409 | |
410 void _reportError(SourceGraph graph, String message, AstNode node) { | |
411 graph._reporter.onError(new AnalysisError(_source, node.offset, node.length, | |
412 new CompileTimeErrorCode('dev_compiler.$runtimeType', message))); | |
413 } | |
414 } | |
415 | |
416 /// Represents a runtime resource from our compiler that is needed to run an | |
417 /// application. | |
418 class ResourceSourceNode extends SourceNode { | |
419 ResourceSourceNode(graph, uri, source) : super(graph, uri, source); | |
420 } | |
421 | |
422 /// Updates the structure and `needsRebuild` marks in nodes of [graph] reachable | |
423 /// from [start]. | |
424 /// | |
425 /// That is, staring from [start], we update the graph by detecting file changes | |
426 /// and rebuilding the structure of the graph wherever it changed (an import was | |
427 /// added or removed, etc). | |
428 /// | |
429 /// After calling this function a node is marked with `needsRebuild` only if it | |
430 /// contained local changes. Rebuild decisions that derive from transitive | |
431 /// changes (e.g. when the API of a dependency changed) are handled later in | |
432 /// [rebuild]. | |
433 void refreshStructureAndMarks(SourceNode start) { | |
434 visitInPreOrder(start, (n) => n.update(), includeParts: false); | |
435 } | |
436 | |
437 /// Clears all the `needsRebuild` and `structureChanged` marks in nodes | |
438 /// reachable from [start]. | |
439 void clearMarks(SourceNode start) { | |
440 visitInPreOrder(start, (n) => n.needsRebuild = n.structureChanged = false, | |
441 includeParts: true); | |
442 } | |
443 | |
444 /// Traverses from [start] with the purpose of building any source that needs to | |
445 /// be rebuilt. | |
446 /// | |
447 /// This function will call [build] in a post-order fashion, on a subset of the | |
448 /// reachable nodes. There are four rules used to decide when to rebuild a node | |
449 /// (call [build] on a node): | |
450 /// | |
451 /// * Only rebuild Dart libraries ([DartSourceNode]) or HTML files | |
452 /// ([HtmlSourceNode]), but skip part files. That is because those are | |
453 /// built as part of some library. | |
454 /// | |
455 /// * Always rebuild [DartSourceNode]s and [HtmlSourceNode]s with local | |
456 /// changes or changes in a part of the library. Internally this function | |
457 /// calls [refreshStructureAndMarks] to ensure that the graph structure is | |
458 /// up-to-date and that these nodes with local changes contain the | |
459 /// `needsRebuild` bit. | |
460 /// | |
461 /// * Rebuild [HtmlSourceNode]s if there were structural changes somewhere | |
462 /// down its reachable subgraph. This is done because HTML files embed the | |
463 /// transitive closure of the import graph in their output. | |
464 /// | |
465 /// * Rebuild [DartSourceNode]s that depend on other [DartSourceNode]s | |
466 /// whose API may have changed. The result of [build] is used to determine | |
467 /// whether other nodes need to be rebuilt. The function [build] is expected | |
468 /// to return `true` on a node `n` if it detemines other nodes that import | |
469 /// `n` may need to be rebuilt as well. | |
470 rebuild(SourceNode start, bool build(SourceNode node)) { | |
471 refreshStructureAndMarks(start); | |
472 // Hold which source nodes may have changed their public API, this includes | |
473 // libraries that were modified or libraries that export other modified APIs. | |
474 // TODO(sigmund): consider removing this special support for exports? Many | |
475 // cases anways require using summaries to understand what parts of the public | |
476 // API may be affected by transitive changes. The re-export case is just one | |
477 // of those transitive cases, but is not sufficient. See | |
478 // https://github.com/dart-lang/dev_compiler/issues/76 | |
479 var apiChangeDetected = new HashSet<SourceNode>(); | |
480 bool htmlNeedsRebuild = false; | |
481 | |
482 bool shouldBuildNode(SourceNode n) { | |
483 if (n.needsRebuild) return true; | |
484 if (n is HtmlSourceNode) return htmlNeedsRebuild; | |
485 if (n is ResourceSourceNode || n is EntryNode) return false; | |
486 return (n as DartSourceNode) | |
487 .imports | |
488 .any((i) => apiChangeDetected.contains(i)); | |
489 } | |
490 | |
491 visitInPostOrder(start, (n) { | |
492 if (n.structureChanged) htmlNeedsRebuild = true; | |
493 if (shouldBuildNode(n)) { | |
494 var oldHash = n.cachingHash; | |
495 if (build(n)) apiChangeDetected.add(n); | |
496 if (oldHash != n.cachingHash) htmlNeedsRebuild = true; | |
497 } else if (n is DartSourceNode && | |
498 n.exports.any((e) => apiChangeDetected.contains(e))) { | |
499 apiChangeDetected.add(n); | |
500 } | |
501 n.needsRebuild = false; | |
502 n.structureChanged = false; | |
503 if (n is DartSourceNode) { | |
504 // Note: clearing out flags in the parts could be a problem if someone | |
505 // tries to use a file both as a part and a library at the same time. | |
506 // In that case, we might not correctly propagate changes in the | |
507 // places where it is used as a library. | |
508 // Technically it's not allowed to have a file as a part and a library | |
509 // at once, and the analyzer should report an error in that case. | |
510 n.parts.forEach((p) => p.needsRebuild = p.structureChanged = false); | |
511 } | |
512 }, includeParts: false); | |
513 } | |
514 | |
515 /// Helper that runs [action] on nodes reachable from [start] in pre-order. | |
516 visitInPreOrder(SourceNode start, void action(SourceNode node), | |
517 {bool includeParts: false}) { | |
518 var seen = new HashSet<SourceNode>(); | |
519 helper(SourceNode node) { | |
520 if (!seen.add(node)) return; | |
521 action(node); | |
522 var deps = includeParts ? node.allDeps : node.depsWithoutParts; | |
523 deps.forEach(helper); | |
524 } | |
525 helper(start); | |
526 } | |
527 | |
528 /// Helper that runs [action] on nodes reachable from [start] in post-order. | |
529 visitInPostOrder(SourceNode start, void action(SourceNode node), | |
530 {bool includeParts: false}) { | |
531 var seen = new HashSet<SourceNode>(); | |
532 helper(SourceNode node) { | |
533 if (!seen.add(node)) return; | |
534 var deps = includeParts ? node.allDeps : node.depsWithoutParts; | |
535 deps.forEach(helper); | |
536 action(node); | |
537 } | |
538 helper(start); | |
539 } | |
540 | |
541 bool _same(Set a, Set b) => a.length == b.length && a.containsAll(b); | |
542 | |
543 final _log = new Logger('dev_compiler.dependency_graph'); | |
OLD | NEW |