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

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

Issue 1788973002: Remove code that requires whole-program compile (Closed) Base URL: git@github.com:dart-lang/dev_compiler.git@master
Patch Set: merged Created 4 years, 9 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
« no previous file with comments | « lib/src/report/html_reporter.dart ('k') | lib/src/server/server.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
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');
OLDNEW
« no previous file with comments | « lib/src/report/html_reporter.dart ('k') | lib/src/server/server.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698