Chromium Code Reviews| 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 library dev_compiler.src.compiler; | |
| 7 | |
| 8 import 'dart:async'; | |
| 9 import 'dart:collection'; | |
| 10 import 'dart:math' as math; | |
| 11 import 'dart:io'; | |
| 12 | |
| 13 import 'package:analyzer/src/generated/ast.dart' show CompilationUnit; | |
| 14 import 'package:analyzer/src/generated/element.dart'; | |
| 15 import 'package:analyzer/src/generated/engine.dart' | |
| 16 show AnalysisEngine, AnalysisContext, ChangeSet, ParseDartTask; | |
| 17 import 'package:analyzer/src/generated/error.dart' | |
| 18 show AnalysisError, ErrorSeverity, ErrorType; | |
| 19 import 'package:analyzer/src/generated/error.dart'; | |
| 20 import 'package:analyzer/src/generated/resolver.dart' show TypeProvider; | |
| 21 import 'package:analyzer/src/generated/source.dart' show Source; | |
| 22 import 'package:analyzer/src/task/html.dart'; | |
| 23 import 'package:html/dom.dart' as html; | |
| 24 import 'package:html/parser.dart' as html; | |
| 25 import 'package:logging/logging.dart' show Level, Logger, LogRecord; | |
| 26 import 'package:path/path.dart' as path; | |
| 27 | |
| 28 import 'package:dev_compiler/strong_mode.dart' show StrongModeOptions; | |
| 29 | |
| 30 import 'analysis_context.dart'; | |
| 31 import 'checker/checker.dart'; | |
| 32 import 'checker/rules.dart'; | |
| 33 import 'codegen/html_codegen.dart' as html_codegen; | |
| 34 import 'codegen/js_codegen.dart'; | |
| 35 import 'info.dart' | |
| 36 show AnalyzerMessage, CheckerResults, LibraryInfo, LibraryUnit; | |
| 37 import 'options.dart'; | |
| 38 import 'report.dart'; | |
| 39 | |
| 40 /// Sets up the type checker logger to print a span that highlights error | |
| 41 /// messages. | |
| 42 StreamSubscription setupLogger(Level level, printFn) { | |
| 43 Logger.root.level = level; | |
| 44 return Logger.root.onRecord.listen((LogRecord rec) { | |
| 45 printFn('${rec.level.name.toLowerCase()}: ${rec.message}'); | |
| 46 }); | |
| 47 } | |
| 48 | |
| 49 class BatchCompiler extends AbstractCompiler { | |
| 50 JSGenerator _jsGen; | |
| 51 | |
| 52 /// Already compiled sources, so we don't compile them again. | |
| 53 final _compiled = new HashSet<LibraryElement>(); | |
| 54 | |
| 55 bool _failure = false; | |
| 56 bool get failure => _failure; | |
| 57 | |
| 58 BatchCompiler(AnalysisContext context, CompilerOptions options, | |
| 59 {AnalysisErrorListener reporter}) | |
| 60 : super(context, options, reporter) { | |
| 61 if (outputDir != null) { | |
| 62 _jsGen = new JSGenerator(this); | |
| 63 } | |
| 64 } | |
| 65 | |
| 66 void reset() { | |
| 67 _compiled.clear(); | |
| 68 } | |
| 69 | |
| 70 /// Compiles every file in [options.inputs]. | |
| 71 /// Returns true on successful compile. | |
| 72 bool run() { | |
| 73 var clock = new Stopwatch()..start(); | |
| 74 options.inputs.forEach(compileFromUriString); | |
| 75 clock.stop(); | |
| 76 var time = (clock.elapsedMilliseconds / 1000).toStringAsFixed(2); | |
| 77 _log.fine('Compiled ${_compiled.length} libraries in ${time} s\n'); | |
| 78 | |
| 79 return !_failure; | |
| 80 } | |
| 81 | |
| 82 void compileFromUriString(String uriString) { | |
| 83 compileFromUri(stringToUri(uriString)); | |
| 84 } | |
| 85 | |
| 86 void compileFromUri(Uri uri) { | |
| 87 var source = context.sourceFactory.forUri(Uri.encodeFull('$uri')); | |
| 88 if (source == null) throw new ArgumentError.value( | |
| 89 uri.toString(), 'uri', 'could not find source for'); | |
| 90 compileSource(source); | |
| 91 } | |
| 92 | |
| 93 void compileSource(Source source) { | |
| 94 if (AnalysisEngine.isHtmlFileName(source.uri.path)) { | |
| 95 compileHtml(source); | |
| 96 return; | |
| 97 } | |
| 98 | |
| 99 compileLibrary(context.computeLibraryElement(source)); | |
| 100 } | |
| 101 | |
| 102 void compileLibrary(LibraryElement library) { | |
| 103 if (!_compiled.add(library)) return; | |
| 104 if (!options.checkSdk && library.source.uri.scheme == 'dart') return; | |
| 105 | |
| 106 // TODO(jmesserly): in incremental mode, we can skip the transitive | |
| 107 // compile of imports/exports. | |
| 108 library.importedLibraries.forEach(compileLibrary); | |
| 109 library.exportedLibraries.forEach(compileLibrary); | |
| 110 | |
| 111 var unitElements = [library.definingCompilationUnit]..addAll(library.parts); | |
| 112 var units = <CompilationUnit>[]; | |
| 113 | |
| 114 bool failureInLib = false; | |
| 115 for (var element in unitElements) { | |
| 116 var unit = context.resolveCompilationUnit(element.source, library); | |
| 117 units.add(unit); | |
| 118 failureInLib = logErrors(element.source) || failureInLib; | |
| 119 checker.visitCompilationUnit(unit); | |
| 120 if (checker.failure) failureInLib = true; | |
| 121 } | |
| 122 | |
| 123 if (failureInLib) { | |
| 124 _failure = true; | |
| 125 if (!options.codegenOptions.forceCompile) return; | |
| 126 } | |
| 127 | |
| 128 if (_jsGen != null) { | |
| 129 var unit = units.first; | |
| 130 var parts = units.skip(1).toList(); | |
| 131 | |
| 132 // TODO(jmesserly): this hack is to avoid compiling the same compilation | |
| 133 // unit to JS twice. We mutate the AST, so it's not safe to run more than | |
| 134 // once on the same unit. | |
| 135 if (unit.getProperty(_propertyName) == true) return; | |
| 136 unit.setProperty(_propertyName, true); | |
| 137 | |
| 138 _jsGen.generateLibrary(new LibraryUnit(unit, parts)); | |
| 139 } | |
| 140 } | |
| 141 | |
| 142 static const String _propertyName = 'dev_compiler.BatchCompiler.isCompiled'; | |
| 143 | |
| 144 void compileHtml(Source source) { | |
| 145 // TODO(jmesserly): reuse DartScriptsTask instead of copy/paste. | |
| 146 var contents = context.getContents(source); | |
| 147 var document = html.parse(contents.data, generateSpans: true); | |
| 148 var scripts = document.querySelectorAll('script[type="application/dart"]'); | |
| 149 | |
| 150 var loadedLibs = new LinkedHashSet<Uri>(); | |
| 151 | |
| 152 for (var script in scripts) { | |
| 153 Source scriptSource = null; | |
| 154 var srcAttr = script.attributes['src']; | |
| 155 if (srcAttr == null) { | |
| 156 if (script.hasContent()) { | |
| 157 var fragments = <ScriptFragment>[]; | |
| 158 for (var node in script.nodes) { | |
| 159 if (node is html.Text) { | |
| 160 var start = node.sourceSpan.start; | |
| 161 fragments.add(new ScriptFragment( | |
| 162 start.offset, start.line, start.column, node.data)); | |
| 163 } | |
| 164 } | |
| 165 scriptSource = new DartScript(source, fragments); | |
| 166 } | |
| 167 } else if (AnalysisEngine.isDartFileName(srcAttr)) { | |
| 168 scriptSource = context.sourceFactory.resolveUri(source, srcAttr); | |
| 169 } | |
| 170 | |
| 171 if (scriptSource != null) { | |
| 172 var lib = context.computeLibraryElement(scriptSource); | |
| 173 compileLibrary(lib); | |
| 174 script.replaceWith(_linkLibraries(lib, loadedLibs)); | |
| 175 } | |
| 176 } | |
| 177 | |
| 178 // TODO(jmesserly): we need to clean this up so we aren't treating these | |
| 179 // as a special case. | |
| 180 for (var file in defaultRuntimeFiles) { | |
| 181 var input = path.join(options.runtimeDir, file); | |
| 182 var output = path.join(outputDir, runtimeFileOutput(file)); | |
| 183 new Directory(path.dirname(output)).createSync(recursive: true); | |
| 184 new File(input).copySync(output); | |
| 185 } | |
| 186 | |
| 187 new File(getOutputPath(source.uri)).openSync(mode: FileMode.WRITE) | |
| 188 ..writeStringSync(document.outerHtml) | |
| 189 ..writeStringSync('\n') | |
| 190 ..closeSync(); | |
| 191 } | |
| 192 | |
| 193 html.DocumentFragment _linkLibraries( | |
| 194 LibraryElement mainLib, LinkedHashSet<Uri> loaded) { | |
| 195 var alreadyLoaded = loaded.length; | |
| 196 _collectLibraries(mainLib, loaded); | |
| 197 | |
| 198 var newLibs = loaded.skip(alreadyLoaded); | |
| 199 var df = new html.DocumentFragment(); | |
| 200 for (var path in defaultRuntimeFiles) { | |
| 201 df.append(html_codegen.libraryInclude(runtimeFileOutput(path))); | |
| 202 } | |
| 203 for (var uri in newLibs) { | |
| 204 if (uri.scheme == 'dart') continue; | |
| 205 df.append(html_codegen.libraryInclude(getModulePath(uri))); | |
| 206 } | |
| 207 df.append(html_codegen.invokeMain(getModuleName(mainLib.source.uri))); | |
| 208 return df; | |
| 209 } | |
| 210 | |
| 211 void _collectLibraries(LibraryElement lib, LinkedHashSet<Uri> loaded) { | |
| 212 var uri = lib.source.uri; | |
| 213 if (!loaded.add(uri)) return; | |
| 214 for (var l in lib.importedLibraries) _collectLibraries(l, loaded); | |
| 215 for (var l in lib.exportedLibraries) _collectLibraries(l, loaded); | |
| 216 // Move the item to the end of the list. | |
| 217 loaded.remove(uri); | |
| 218 loaded.add(uri); | |
| 219 } | |
| 220 | |
| 221 String runtimeFileOutput(String file) => | |
| 222 path.join('dev_compiler', 'runtime', file); | |
| 223 } | |
| 224 | |
| 225 abstract class AbstractCompiler { | |
| 226 final CompilerOptions options; | |
| 227 final AnalysisContext context; | |
| 228 final CodeChecker checker; | |
| 229 | |
| 230 AbstractCompiler(AnalysisContext context, CompilerOptions options, | |
| 231 [AnalysisErrorListener reporter]) | |
| 232 : context = context, | |
| 233 options = options, | |
| 234 checker = createChecker(context.typeProvider, options.strongOptions, | |
| 235 reporter == null ? AnalysisErrorListener.NULL_LISTENER : reporter) { | |
| 236 enableDevCompilerInference(context, options.strongOptions); | |
| 237 } | |
| 238 | |
| 239 static CodeChecker createChecker(TypeProvider typeProvider, | |
| 240 StrongModeOptions options, AnalysisErrorListener reporter) { | |
| 241 return new CodeChecker( | |
| 242 new RestrictedRules(typeProvider, options: options), reporter, options); | |
| 243 } | |
| 244 | |
| 245 String get outputDir => options.codegenOptions.outputDir; | |
| 246 TypeRules get rules => checker.rules; | |
| 247 AnalysisErrorListener get reporter => checker.reporter; | |
| 248 | |
| 249 Uri stringToUri(String uriString) { | |
| 250 var uri = uriString.startsWith('dart:') || uriString.startsWith('package:') | |
| 251 ? Uri.parse(uriString) | |
| 252 : new Uri.file(uriString); | |
| 253 return uri; | |
| 254 } | |
| 255 | |
| 256 /// Directory presumed to be the common prefix for all input file:// URIs. | |
| 257 /// Used when computing output paths. | |
| 258 /// | |
| 259 /// For example: | |
| 260 /// dartdevc -o out foo/a.dart bar/b.dart | |
| 261 /// | |
| 262 /// Will produce: | |
| 263 /// out/foo/a.dart | |
| 264 /// out/bar/b.dart | |
| 265 /// | |
| 266 /// This is only used if at least one of [options.codegenOptions.inputs] is | |
| 267 /// a file URI. | |
| 268 // TODO(jmesserly): do we need an option for this? | |
|
vsm
2015/07/16 23:56:39
Might be nice at some point, but easy enough to ad
| |
| 269 // Other ideas: we could look up and see what package the file is in, treat | |
| 270 // that as a base path. We could also use the current working directory as | |
| 271 // the base. | |
| 272 String get inputBaseDir { | |
| 273 if (_fileUriCommonBaseDir == null) { | |
| 274 List<String> common = null; | |
| 275 for (var uri in options.inputs.map(stringToUri)) { | |
| 276 if (uri.scheme != 'file') continue; | |
| 277 | |
| 278 var segments = path.split(path.dirname(uri.path)); | |
| 279 if (common == null) { | |
| 280 common = segments; | |
| 281 } else { | |
| 282 int len = math.min(common.length, segments.length); | |
| 283 while (len > 0 && common[len - 1] != segments[len - 1]) { | |
| 284 len--; | |
| 285 } | |
| 286 common.length = len; | |
| 287 } | |
| 288 } | |
| 289 _fileUriCommonBaseDir = common == null ? '' : path.joinAll(common); | |
| 290 } | |
| 291 return _fileUriCommonBaseDir; | |
| 292 } | |
| 293 String _fileUriCommonBaseDir; | |
| 294 | |
| 295 String getOutputPath(Uri uri) => path.join(outputDir, getModulePath(uri)); | |
| 296 | |
| 297 /// Like [getModuleName] but includes the file extension, either .js or .html. | |
| 298 String getModulePath(Uri uri) { | |
| 299 var ext = path.extension(uri.path); | |
| 300 if (ext == '.dart' || ext == '' && uri.scheme == 'dart') ext = '.js'; | |
| 301 return getModuleName(uri) + ext; | |
| 302 } | |
| 303 | |
| 304 /// Gets the module name, without extension. For example: | |
| 305 /// | |
| 306 /// * dart:core -> dart/core | |
| 307 /// * file:foo/bar/baz.dart -> foo/bar/baz | |
| 308 /// * package:qux/qux.dart -> qux/qux | |
|
vsm
2015/07/16 23:56:39
at some point, we might want to prefix the package
| |
| 309 /// | |
| 310 /// For file: URLs this will also make them relative to [inputBaseDir]. | |
| 311 String getModuleName(Uri uri) { | |
| 312 var filepath = path.withoutExtension(uri.path); | |
| 313 if (uri.scheme == 'dart') { | |
| 314 filepath = 'dart/$filepath'; | |
| 315 } else if (uri.scheme == 'file') { | |
| 316 filepath = path.relative(filepath, from: inputBaseDir); | |
| 317 } else { | |
| 318 assert(uri.scheme == 'package'); | |
| 319 // filepath is good here, we want the output to start with a directory | |
| 320 // matching the package name. | |
| 321 } | |
| 322 return filepath; | |
| 323 } | |
| 324 | |
| 325 /// Log any errors encountered when resolving [source] and return whether any | |
| 326 /// errors were found. | |
| 327 bool logErrors(Source source) { | |
| 328 List<AnalysisError> errors = context.computeErrors(source); | |
| 329 bool failure = false; | |
| 330 if (errors.isNotEmpty) { | |
| 331 for (var error in errors) { | |
| 332 // Always skip TODOs. | |
| 333 if (error.errorCode.type == ErrorType.TODO) continue; | |
| 334 | |
| 335 // Skip hints for now. In the future these could be turned on via flags. | |
| 336 if (error.errorCode.errorSeverity.ordinal < | |
| 337 ErrorSeverity.WARNING.ordinal) { | |
| 338 continue; | |
| 339 } | |
| 340 | |
| 341 // All analyzer warnings or errors are errors for DDC. | |
| 342 failure = true; | |
| 343 reporter.onError(error); | |
| 344 } | |
| 345 } | |
| 346 return failure; | |
| 347 } | |
| 348 } | |
| 349 | |
| 350 AnalysisErrorListener createErrorReporter( | |
| 351 AnalysisContext context, CompilerOptions options) { | |
| 352 return options.dumpInfo | |
| 353 ? new SummaryReporter(context, options.logLevel) | |
| 354 : new LogReporter(context, useColors: options.useColors); | |
| 355 } | |
| 356 | |
| 357 // TODO(jmesserly): find a better home for these. | |
| 358 /// Curated order to minimize lazy classes needed by dart:core and its | |
| 359 /// transitive SDK imports. | |
| 360 const corelibOrder = const [ | |
| 361 'dart.core', | |
| 362 'dart.collection', | |
| 363 'dart._internal', | |
| 364 'dart.math', | |
| 365 'dart._interceptors', | |
| 366 'dart.async', | |
| 367 'dart._foreign_helper', | |
| 368 'dart._js_embedded_names', | |
| 369 'dart._js_helper', | |
| 370 'dart.isolate', | |
| 371 'dart.typed_data', | |
| 372 'dart._native_typed_data', | |
| 373 'dart._isolate_helper', | |
| 374 'dart._js_primitives', | |
| 375 'dart.convert', | |
| 376 'dart.mirrors', | |
| 377 'dart._js_mirrors', | |
| 378 'dart.js' | |
| 379 // _foreign_helper is not included, as it only defines the JS builtin that | |
| 380 // the compiler handles at compile time. | |
| 381 ]; | |
| 382 | |
| 383 /// Runtime files added to all applications when running the compiler in the | |
| 384 /// command line. | |
| 385 final defaultRuntimeFiles = () { | |
| 386 var files = [ | |
| 387 'harmony_feature_check.js', | |
| 388 'dart_utils.js', | |
| 389 'dart_library.js', | |
| 390 '_errors.js', | |
| 391 '_types.js', | |
| 392 '_rtti.js', | |
| 393 '_classes.js', | |
| 394 '_operations.js', | |
| 395 'dart_runtime.js', | |
| 396 ]; | |
| 397 files.addAll(corelibOrder.map((l) => l.replaceAll('.', '/') + '.js')); | |
| 398 return files; | |
| 399 }(); | |
| 400 | |
| 401 final _log = new Logger('dev_compiler.src.compiler'); | |
| OLD | NEW |