Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(172)

Side by Side Diff: lib/src/dependency_graph.dart

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

Powered by Google App Engine
This is Rietveld 408576698