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 /// Command line tool to run the checker on a Dart program. | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:collection'; | |
9 import 'dart:math' as math; | |
10 import 'dart:io'; | |
11 | |
12 import 'package:analyzer/dart/ast/ast.dart' show CompilationUnit; | |
13 import 'package:analyzer/dart/element/element.dart'; | |
14 import 'package:analyzer/src/generated/engine.dart' | |
15 show AnalysisEngine, AnalysisContext, ChangeSet, ParseDartTask; | |
16 import 'package:analyzer/src/generated/error.dart' | |
17 show AnalysisError, ErrorSeverity, ErrorType; | |
18 import 'package:analyzer/src/generated/error.dart'; | |
19 import 'package:analyzer/src/generated/source.dart' show Source; | |
20 import 'package:analyzer/src/task/html.dart'; | |
21 import 'package:html/dom.dart' as html; | |
22 import 'package:html/parser.dart' as html; | |
23 import 'package:logging/logging.dart' show Level, Logger, LogRecord; | |
24 import 'package:path/path.dart' as path; | |
25 | |
26 import 'analysis_context.dart'; | |
27 import 'codegen/html_codegen.dart' as html_codegen; | |
28 import 'codegen/js_codegen.dart'; | |
29 import 'options.dart'; | |
30 import 'report.dart'; | |
31 import 'utils.dart' show FileSystem, isStrongModeError; | |
32 | |
33 /// Sets up the type checker logger to print a span that highlights error | |
34 /// messages. | |
35 StreamSubscription setupLogger(Level level, printFn) { | |
36 Logger.root.level = level; | |
37 return Logger.root.onRecord.listen((LogRecord rec) { | |
38 printFn('${rec.level.name.toLowerCase()}: ${rec.message}'); | |
39 }); | |
40 } | |
41 | |
42 CompilerOptions validateOptions(List<String> args, {bool forceOutDir: false}) { | |
43 var options = parseOptions(args, forceOutDir: forceOutDir); | |
44 if (!options.help && !options.version) { | |
45 var srcOpts = options.sourceOptions; | |
46 if (!srcOpts.useMockSdk && srcOpts.dartSdkPath == null) { | |
47 print('Could not automatically find dart sdk path.'); | |
48 print('Please pass in explicitly: --dart-sdk <path>'); | |
49 exit(1); | |
50 } | |
51 if (options.inputs.length == 0) { | |
52 print('Expected filename.'); | |
53 return null; | |
54 } | |
55 } | |
56 return options; | |
57 } | |
58 | |
59 /// Compile with the given options and return success or failure. | |
60 bool compile(CompilerOptions options) { | |
61 var context = createAnalysisContextWithSources(options.sourceOptions); | |
62 var reporter = new LogReporter(context, useColors: options.useColors); | |
63 return new BatchCompiler(context, options, reporter: reporter).run(); | |
64 } | |
65 | |
66 // Callback on each individual compiled library | |
67 typedef void CompilationNotifier(String path); | |
68 | |
69 class BatchCompiler extends AbstractCompiler { | |
70 JSGenerator _jsGen; | |
71 LibraryElement _dartCore; | |
72 String _runtimeOutputDir; | |
73 | |
74 /// Already compiled sources, so we don't check or compile them again. | |
75 final _compilationRecord = <LibraryElement, bool>{}; | |
76 bool _sdkCopied = false; | |
77 | |
78 bool _failure = false; | |
79 bool get failure => _failure; | |
80 | |
81 final _pendingLibraries = <List<CompilationUnit>>[]; | |
82 | |
83 BatchCompiler(AnalysisContext context, CompilerOptions options, | |
84 {AnalysisErrorListener reporter, | |
85 FileSystem fileSystem: const FileSystem()}) | |
86 : super( | |
87 context, | |
88 options, | |
89 new ErrorCollector( | |
90 context, reporter ?? AnalysisErrorListener.NULL_LISTENER), | |
91 fileSystem) { | |
92 _inputBaseDir = options.inputBaseDir; | |
93 if (outputDir != null) { | |
94 _jsGen = new JSGenerator(this); | |
95 _runtimeOutputDir = path.join(outputDir, 'dev_compiler', 'runtime'); | |
96 } | |
97 _dartCore = context.typeProvider.objectType.element.library; | |
98 } | |
99 | |
100 ErrorCollector get reporter => super.reporter; | |
101 | |
102 /// Compiles every file in [options.inputs]. | |
103 /// Returns true on successful compile. | |
104 bool run() { | |
105 var clock = new Stopwatch()..start(); | |
106 options.inputs.forEach(compileFromUriString); | |
107 clock.stop(); | |
108 var time = (clock.elapsedMilliseconds / 1000).toStringAsFixed(2); | |
109 _log.fine('Compiled ${_compilationRecord.length} libraries in ${time} s\n'); | |
110 | |
111 return !_failure; | |
112 } | |
113 | |
114 void compileFromUriString(String uriString, [CompilationNotifier notifier]) { | |
115 _compileFromUri(stringToUri(uriString), notifier); | |
116 } | |
117 | |
118 void _compileFromUri(Uri uri, CompilationNotifier notifier) { | |
119 _failure = false; | |
120 if (!uri.isAbsolute) { | |
121 throw new ArgumentError.value('$uri', 'uri', 'must be absolute'); | |
122 } | |
123 var source = context.sourceFactory.forUri(Uri.encodeFull('$uri')); | |
124 if (source == null) { | |
125 throw new ArgumentError.value('$uri', 'uri', 'could not find source for'); | |
126 } | |
127 _compileSource(source, notifier); | |
128 } | |
129 | |
130 void _compileSource(Source source, CompilationNotifier notifier) { | |
131 if (AnalysisEngine.isHtmlFileName(source.uri.path)) { | |
132 _compileHtml(source, notifier); | |
133 } else { | |
134 _compileLibrary(context.computeLibraryElement(source), notifier); | |
135 } | |
136 _processPending(); | |
137 reporter.flush(); | |
138 } | |
139 | |
140 void _processPending() { | |
141 // _pendingLibraries was recorded in post-order. Process from the end | |
142 // to ensure reverse post-order. This will ensure that we handle back | |
143 // edges from the original depth-first search correctly. | |
144 | |
145 while (_pendingLibraries.isNotEmpty) { | |
146 var unit = _pendingLibraries.removeLast(); | |
147 var library = unit.first.element.library; | |
148 assert(_compilationRecord[library] == true || | |
149 options.codegenOptions.forceCompile); | |
150 | |
151 // Process dependencies one more time to propagate failure from cycles | |
152 for (var import in library.imports) { | |
153 if (!_compilationRecord[import.importedLibrary]) { | |
154 _compilationRecord[library] = false; | |
155 } | |
156 } | |
157 for (var export in library.exports) { | |
158 if (!_compilationRecord[export.exportedLibrary]) { | |
159 _compilationRecord[library] = false; | |
160 } | |
161 } | |
162 | |
163 // Generate code if still valid | |
164 if (_jsGen != null && | |
165 (_compilationRecord[library] || | |
166 options.codegenOptions.forceCompile)) { | |
167 _jsGen.generateLibrary(unit); | |
168 } | |
169 } | |
170 } | |
171 | |
172 bool _compileLibrary(LibraryElement library, CompilationNotifier notifier) { | |
173 var success = _compilationRecord[library]; | |
174 if (success != null) { | |
175 if (!success) _failure = true; | |
176 return success; | |
177 } | |
178 | |
179 // Optimistically mark a library valid until proven otherwise | |
180 _compilationRecord[library] = true; | |
181 | |
182 if (!options.checkSdk && library.source.isInSystemLibrary) { | |
183 // We assume the Dart SDK is always valid | |
184 if (_jsGen != null) _copyDartRuntime(); | |
185 return true; | |
186 } | |
187 | |
188 // Check dependences to determine if this library type checks | |
189 // TODO(jmesserly): in incremental mode, we can skip the transitive | |
190 // compile of imports/exports. | |
191 _compileLibrary(_dartCore, notifier); // implicit dart:core dependency | |
192 for (var import in library.imports) { | |
193 if (!_compileLibrary(import.importedLibrary, notifier)) { | |
194 _compilationRecord[library] = false; | |
195 } | |
196 } | |
197 for (var export in library.exports) { | |
198 if (!_compileLibrary(export.exportedLibrary, notifier)) { | |
199 _compilationRecord[library] = false; | |
200 } | |
201 } | |
202 | |
203 // Check this library's own code | |
204 var unitElements = new List.from(library.parts) | |
205 ..add(library.definingCompilationUnit); | |
206 var units = <CompilationUnit>[]; | |
207 | |
208 bool failureInLib = false; | |
209 for (var element in unitElements) { | |
210 var unit = context.resolveCompilationUnit(element.source, library); | |
211 units.add(unit); | |
212 failureInLib = computeErrors(element.source) || failureInLib; | |
213 } | |
214 if (failureInLib) _compilationRecord[library] = false; | |
215 | |
216 // Notifier framework if requested | |
217 if (notifier != null) { | |
218 reporter.flush(); | |
219 notifier(getOutputPath(library.source.uri)); | |
220 } | |
221 | |
222 // Record valid libraries for further dependence checking (cycles) and | |
223 // codegen. | |
224 | |
225 // TODO(vsm): Restructure this to not delay code generation more than | |
226 // necessary. We'd like to process the AST before there is any chance | |
227 // it's cached out. We should refactor common logic in | |
228 // server/dependency_graph and perhaps the analyzer itself. | |
229 success = _compilationRecord[library]; | |
230 if (success || options.codegenOptions.forceCompile) { | |
231 _pendingLibraries.add(units); | |
232 } | |
233 | |
234 // Return tentative success status. | |
235 if (!success) _failure = true; | |
236 return success; | |
237 } | |
238 | |
239 void _copyDartRuntime() { | |
240 if (_sdkCopied) return; | |
241 _sdkCopied = true; | |
242 for (var file in defaultRuntimeFiles) { | |
243 var input = new File(path.join(options.runtimeDir, file)); | |
244 var output = new File(path.join(_runtimeOutputDir, file)); | |
245 if (output.existsSync() && | |
246 output.lastModifiedSync() == input.lastModifiedSync()) { | |
247 continue; | |
248 } | |
249 fileSystem.copySync(input.path, output.path); | |
250 } | |
251 } | |
252 | |
253 void _compileHtml(Source source, CompilationNotifier notifier) { | |
254 // TODO(jmesserly): reuse DartScriptsTask instead of copy/paste. | |
255 var contents = context.getContents(source); | |
256 var document = html.parse(contents.data, generateSpans: true); | |
257 var scripts = document.querySelectorAll('script[type="application/dart"]'); | |
258 | |
259 var loadedLibs = new LinkedHashSet<Uri>(); | |
260 | |
261 // If we're generating code, convert the HTML file as well. | |
262 // Otherwise, just search for Dart sources to analyze. | |
263 var htmlOutDir = | |
264 _jsGen != null ? path.dirname(getOutputPath(source.uri)) : null; | |
265 for (var script in scripts) { | |
266 Source scriptSource = null; | |
267 var srcAttr = script.attributes['src']; | |
268 if (srcAttr == null) { | |
269 if (script.hasContent()) { | |
270 var fragments = <ScriptFragment>[]; | |
271 for (var node in script.nodes) { | |
272 if (node is html.Text) { | |
273 var start = node.sourceSpan.start; | |
274 fragments.add(new ScriptFragment( | |
275 start.offset, start.line, start.column, node.data)); | |
276 } | |
277 } | |
278 scriptSource = new DartScript(source, fragments); | |
279 } | |
280 } else if (AnalysisEngine.isDartFileName(srcAttr)) { | |
281 scriptSource = context.sourceFactory.resolveUri(source, srcAttr); | |
282 } | |
283 | |
284 if (scriptSource != null) { | |
285 var lib = context.computeLibraryElement(scriptSource); | |
286 _compileLibrary(lib, notifier); | |
287 if (htmlOutDir != null) { | |
288 script.replaceWith(_linkLibraries(lib, loadedLibs, from: htmlOutDir)); | |
289 } | |
290 } | |
291 } | |
292 | |
293 if (htmlOutDir != null) { | |
294 fileSystem.writeAsStringSync( | |
295 getOutputPath(source.uri), document.outerHtml + '\n'); | |
296 } | |
297 } | |
298 | |
299 html.DocumentFragment _linkLibraries( | |
300 LibraryElement mainLib, LinkedHashSet<Uri> loaded, | |
301 {String from}) { | |
302 assert(from != null); | |
303 var alreadyLoaded = loaded.length; | |
304 _collectLibraries(mainLib, loaded); | |
305 | |
306 var newLibs = loaded.skip(alreadyLoaded); | |
307 var df = new html.DocumentFragment(); | |
308 | |
309 for (var uri in newLibs) { | |
310 if (uri.scheme == 'dart') { | |
311 if (uri.path == 'core') { | |
312 // TODO(jmesserly): it would be nice to not special case these. | |
313 for (var file in defaultRuntimeFiles) { | |
314 file = path.join(_runtimeOutputDir, file); | |
315 df.append( | |
316 html_codegen.libraryInclude(path.relative(file, from: from))); | |
317 } | |
318 } | |
319 } else { | |
320 var file = path.join(outputDir, getModulePath(uri)); | |
321 df.append(html_codegen.libraryInclude(path.relative(file, from: from))); | |
322 } | |
323 } | |
324 | |
325 df.append(html_codegen.invokeMain(getModuleName(mainLib.source.uri))); | |
326 return df; | |
327 } | |
328 | |
329 void _collectLibraries(LibraryElement lib, LinkedHashSet<Uri> loaded) { | |
330 var uri = lib.source.uri; | |
331 if (!loaded.add(uri)) return; | |
332 _collectLibraries(_dartCore, loaded); | |
333 | |
334 for (var l in lib.imports) _collectLibraries(l.importedLibrary, loaded); | |
335 for (var l in lib.exports) _collectLibraries(l.exportedLibrary, loaded); | |
336 // Move the item to the end of the list. | |
337 loaded.remove(uri); | |
338 loaded.add(uri); | |
339 } | |
340 } | |
341 | |
342 abstract class AbstractCompiler { | |
343 final CompilerOptions options; | |
344 final AnalysisContext context; | |
345 final AnalysisErrorListener reporter; | |
346 final FileSystem fileSystem; | |
347 | |
348 AbstractCompiler(this.context, this.options, | |
349 [AnalysisErrorListener listener, this.fileSystem = const FileSystem()]) | |
350 : reporter = listener ?? AnalysisErrorListener.NULL_LISTENER; | |
351 | |
352 String get outputDir => options.codegenOptions.outputDir; | |
353 | |
354 Uri stringToUri(String uriString) { | |
355 var uri = uriString.startsWith('dart:') || uriString.startsWith('package:') | |
356 ? Uri.parse(uriString) | |
357 : new Uri.file(path.absolute(uriString)); | |
358 return uri; | |
359 } | |
360 | |
361 /// Directory presumed to be the common prefix for all input file:// URIs. | |
362 /// Used when computing output paths. | |
363 /// | |
364 /// For example: | |
365 /// dartdevc -o out foo/a.dart bar/b.dart | |
366 /// | |
367 /// Will produce: | |
368 /// out/foo/a.dart | |
369 /// out/bar/b.dart | |
370 /// | |
371 /// This is only used if at least one of [options.codegenOptions.inputs] is | |
372 /// a file URI. | |
373 // TODO(jmesserly): do we need an option for this? | |
374 // Other ideas: we could look up and see what package the file is in, treat | |
375 // that as a base path. We could also use the current working directory as | |
376 // the base. | |
377 String get inputBaseDir { | |
378 if (_inputBaseDir == null) { | |
379 List<String> common = null; | |
380 for (var uri in options.inputs.map(stringToUri)) { | |
381 if (uri.scheme != 'file') continue; | |
382 | |
383 var segments = path.split(path.dirname(uri.path)); | |
384 if (common == null) { | |
385 common = segments; | |
386 } else { | |
387 int len = math.min(common.length, segments.length); | |
388 while (len > 0 && common[len - 1] != segments[len - 1]) { | |
389 len--; | |
390 } | |
391 common.length = len; | |
392 } | |
393 } | |
394 _inputBaseDir = common == null ? '' : path.joinAll(common); | |
395 } | |
396 return _inputBaseDir; | |
397 } | |
398 | |
399 String _inputBaseDir; | |
400 | |
401 String getOutputPath(Uri uri) => path.join(outputDir, getModulePath(uri)); | |
402 | |
403 /// Like [getModuleName] but includes the file extension, either .js or .html. | |
404 String getModulePath(Uri uri) { | |
405 var ext = path.extension(uri.path); | |
406 if (ext == '.dart' || ext == '' && uri.scheme == 'dart') ext = '.js'; | |
407 return getModuleName(uri) + ext; | |
408 } | |
409 | |
410 /// Gets the module name, without extension. For example: | |
411 /// | |
412 /// * dart:core -> dart/core | |
413 /// * file:foo/bar/baz.dart -> foo/bar/baz | |
414 /// * package:qux/qux.dart -> qux/qux | |
415 /// | |
416 /// For file: URLs this will also make them relative to [inputBaseDir]. | |
417 // TODO(jmesserly): we need to figure out a way to keep package and file URLs | |
418 // from conflicting. | |
419 String getModuleName(Uri uri) { | |
420 var filepath = path.withoutExtension(uri.path); | |
421 if (uri.scheme == 'dart') { | |
422 return 'dart/$filepath'; | |
423 } else if (uri.scheme == 'file') { | |
424 return path.relative(filepath, from: inputBaseDir); | |
425 } else { | |
426 assert(uri.scheme == 'package'); | |
427 // filepath is good here, we want the output to start with a directory | |
428 // matching the package name. | |
429 return filepath; | |
430 } | |
431 } | |
432 | |
433 /// Log any errors encountered when resolving [source] and return whether any | |
434 /// errors were found. | |
435 bool computeErrors(Source source) { | |
436 AnalysisContext errorContext = context; | |
437 // TODO(jmesserly): should this be a fix somewhere in analyzer? | |
438 // otherwise we fail to find the parts. | |
439 if (source.isInSystemLibrary) { | |
440 errorContext = context.sourceFactory.dartSdk.context; | |
441 } | |
442 List<AnalysisError> errors = errorContext.computeErrors(source); | |
443 bool failure = false; | |
444 for (var error in errors) { | |
445 // TODO(jmesserly): this is a very expensive lookup, and it has to be | |
446 // repeated every time we want to query error severity. | |
447 var severity = errorSeverity(errorContext, error); | |
448 if (severity == ErrorSeverity.ERROR) { | |
449 reporter.onError(error); | |
450 failure = true; | |
451 } else if (severity == ErrorSeverity.WARNING) { | |
452 reporter.onError(error); | |
453 } | |
454 } | |
455 return failure; | |
456 } | |
457 } | |
458 | |
459 // TODO(jmesserly): find a better home for these. | |
460 /// Curated order to minimize lazy classes needed by dart:core and its | |
461 /// transitive SDK imports. | |
462 final corelibOrder = [ | |
463 'dart:core', | |
464 'dart:collection', | |
465 'dart:_internal', | |
466 'dart:math', | |
467 'dart:_interceptors', | |
468 'dart:async', | |
469 'dart:_foreign_helper', | |
470 'dart:_js_embedded_names', | |
471 'dart:_js_helper', | |
472 'dart:isolate', | |
473 'dart:typed_data', | |
474 'dart:_native_typed_data', | |
475 'dart:_isolate_helper', | |
476 'dart:_js_primitives', | |
477 'dart:convert', | |
478 // TODO(jmesserly): these are not part of corelib library cycle, and shouldn't | |
479 // be listed here. Instead, their source should be copied on demand if they | |
480 // are actually used by the application. | |
481 'dart:mirrors', | |
482 'dart:_js_mirrors', | |
483 'dart:js', | |
484 'dart:_metadata', | |
485 'dart:html', | |
486 'dart:html_common', | |
487 'dart:indexed_db', | |
488 'dart:svg', | |
489 'dart:web_audio', | |
490 'dart:web_gl', | |
491 'dart:web_sql', | |
492 'dart:_debugger' | |
493 | |
494 // _foreign_helper is not included, as it only defines the JS builtin that | |
495 // the compiler handles at compile time. | |
496 ].map(Uri.parse).toList(); | |
497 | |
498 /// Runtime files added to all applications when running the compiler in the | |
499 /// command line. | |
500 final defaultRuntimeFiles = () { | |
501 String coreToFile(Uri uri) => uri.toString().replaceAll(':', '/') + '.js'; | |
502 | |
503 var files = [ | |
504 'harmony_feature_check.js', | |
505 'dart_library.js', | |
506 'dart/_runtime.js', | |
507 ]; | |
508 files.addAll(corelibOrder.map(coreToFile)); | |
509 return files; | |
510 }(); | |
511 | |
512 final _log = new Logger('dev_compiler.src.compiler'); | |
OLD | NEW |