blob: 5375a3575570a49bc43441dd4ad941bf2d5d18b9 [file] [log] [blame] [edit]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Command line tool to run the checker on a Dart program.
import 'dart:async';
import 'dart:collection';
import 'dart:convert' show JSON;
import 'dart:math' as math;
import 'dart:io';
import 'package:analyzer/src/generated/ast.dart' show CompilationUnit;
import 'package:analyzer/src/generated/element.dart';
import 'package:analyzer/src/generated/engine.dart'
show AnalysisEngine, AnalysisContext, ChangeSet, ParseDartTask;
import 'package:analyzer/src/generated/error.dart'
show AnalysisError, ErrorSeverity, ErrorType;
import 'package:analyzer/src/generated/error.dart';
import 'package:analyzer/src/generated/source.dart' show Source;
import 'package:analyzer/src/task/html.dart';
import 'package:html/dom.dart' as html;
import 'package:html/parser.dart' as html;
import 'package:logging/logging.dart' show Level, Logger, LogRecord;
import 'package:path/path.dart' as path;
import 'analysis_context.dart';
import 'codegen/html_codegen.dart' as html_codegen;
import 'codegen/js_codegen.dart';
import 'info.dart'
show AnalyzerMessage, CheckerResults, LibraryInfo, LibraryUnit;
import 'options.dart';
import 'report.dart';
import 'report/html_reporter.dart';
import 'utils.dart' show FileSystem, isStrongModeError;
/// Sets up the type checker logger to print a span that highlights error
/// messages.
StreamSubscription setupLogger(Level level, printFn) {
Logger.root.level = level;
return Logger.root.onRecord.listen((LogRecord rec) {
printFn('${rec.level.name.toLowerCase()}: ${rec.message}');
});
}
CompilerOptions validateOptions(List<String> args, {bool forceOutDir: false}) {
var options = parseOptions(args, forceOutDir: forceOutDir);
if (!options.help && !options.version) {
var srcOpts = options.sourceOptions;
if (!srcOpts.useMockSdk && srcOpts.dartSdkPath == null) {
print('Could not automatically find dart sdk path.');
print('Please pass in explicitly: --dart-sdk <path>');
exit(1);
}
if (options.inputs.length == 0) {
print('Expected filename.');
return null;
}
}
return options;
}
/// Compile with the given options and return success or failure.
bool compile(CompilerOptions options) {
assert(!options.serverMode);
var context = createAnalysisContextWithSources(options.sourceOptions);
var reporter = createErrorReporter(context, options);
bool status = new BatchCompiler(context, options, reporter: reporter).run();
if (reporter is HtmlReporter) {
reporter.finish(options);
} else if (options.dumpInfo && reporter is SummaryReporter) {
var result = reporter.result;
print(summaryToString(result));
if (options.dumpInfoFile != null) {
var file = new File(options.dumpInfoFile);
file.writeAsStringSync(JSON.encode(result.toJsonMap()));
}
}
return status;
}
// Callback on each individual compiled library
typedef void CompilationNotifier(String path);
class BatchCompiler extends AbstractCompiler {
JSGenerator _jsGen;
LibraryElement _dartCore;
String _runtimeOutputDir;
/// Already compiled sources, so we don't check or compile them again.
final _compilationRecord = <LibraryElement, bool>{};
bool _sdkCopied = false;
bool _failure = false;
bool get failure => _failure;
final _pendingLibraries = <LibraryUnit>[];
BatchCompiler(AnalysisContext context, CompilerOptions options,
{AnalysisErrorListener reporter,
FileSystem fileSystem: const FileSystem()})
: super(
context,
options,
new ErrorCollector(reporter ?? AnalysisErrorListener.NULL_LISTENER),
fileSystem) {
_inputBaseDir = options.inputBaseDir;
if (outputDir != null) {
_jsGen = new JSGenerator(this);
_runtimeOutputDir = path.join(outputDir, 'dev_compiler', 'runtime');
}
_dartCore = context.typeProvider.objectType.element.library;
}
ErrorCollector get reporter => super.reporter;
/// Compiles every file in [options.inputs].
/// Returns true on successful compile.
bool run() {
var clock = new Stopwatch()..start();
options.inputs.forEach(compileFromUriString);
clock.stop();
var time = (clock.elapsedMilliseconds / 1000).toStringAsFixed(2);
_log.fine('Compiled ${_compilationRecord.length} libraries in ${time} s\n');
return !_failure;
}
void compileFromUriString(String uriString, [CompilationNotifier notifier]) {
_compileFromUri(stringToUri(uriString), notifier);
}
void _compileFromUri(Uri uri, CompilationNotifier notifier) {
_failure = false;
if (!uri.isAbsolute) {
throw new ArgumentError.value('$uri', 'uri', 'must be absolute');
}
var source = context.sourceFactory.forUri(Uri.encodeFull('$uri'));
if (source == null) {
throw new ArgumentError.value('$uri', 'uri', 'could not find source for');
}
_compileSource(source, notifier);
}
void _compileSource(Source source, CompilationNotifier notifier) {
if (AnalysisEngine.isHtmlFileName(source.uri.path)) {
_compileHtml(source, notifier);
} else {
_compileLibrary(context.computeLibraryElement(source), notifier);
}
_processPending();
reporter.flush();
}
void _processPending() {
// _pendingLibraries was recorded in post-order. Process from the end
// to ensure reverse post-order. This will ensure that we handle back
// edges from the original depth-first search correctly.
while (_pendingLibraries.isNotEmpty) {
var unit = _pendingLibraries.removeLast();
var library = unit.library.element.enclosingElement;
assert(_compilationRecord[library] == true ||
options.codegenOptions.forceCompile);
// Process dependences one more time to propagate failure from cycles
for (var import in library.imports) {
if (!_compilationRecord[import.importedLibrary]) {
_compilationRecord[library] = false;
}
}
for (var export in library.exports) {
if (!_compilationRecord[export.exportedLibrary]) {
_compilationRecord[library] = false;
}
}
// Generate code if still valid
if (_jsGen != null &&
(_compilationRecord[library] ||
options.codegenOptions.forceCompile)) {
_jsGen.generateLibrary(unit);
}
}
}
bool _compileLibrary(LibraryElement library, CompilationNotifier notifier) {
var success = _compilationRecord[library];
if (success != null) {
if (!success) _failure = true;
return success;
}
// Optimistically mark a library valid until proven otherwise
_compilationRecord[library] = true;
if (!options.checkSdk && library.source.uri.scheme == 'dart') {
// We assume the Dart SDK is always valid
if (_jsGen != null) _copyDartRuntime();
return true;
}
// Check dependences to determine if this library type checks
// TODO(jmesserly): in incremental mode, we can skip the transitive
// compile of imports/exports.
_compileLibrary(_dartCore, notifier); // implicit dart:core dependency
for (var import in library.imports) {
if (!_compileLibrary(import.importedLibrary, notifier)) {
_compilationRecord[library] = false;
}
}
for (var export in library.exports) {
if (!_compileLibrary(export.exportedLibrary, notifier)) {
_compilationRecord[library] = false;
}
}
// Check this library's own code
var unitElements = [library.definingCompilationUnit]..addAll(library.parts);
var units = <CompilationUnit>[];
bool failureInLib = false;
for (var element in unitElements) {
var unit = context.resolveCompilationUnit(element.source, library);
units.add(unit);
failureInLib = computeErrors(element.source) || failureInLib;
}
if (failureInLib) _compilationRecord[library] = false;
// Notifier framework if requested
if (notifier != null) {
reporter.flush();
notifier(getOutputPath(library.source.uri));
}
// Record valid libraries for further dependence checking (cycles) and
// codegen.
// TODO(vsm): Restructure this to not delay code generation more than
// necessary. We'd like to process the AST before there is any chance
// it's cached out. We should refactor common logic in
// server/dependency_graph and perhaps the analyzer itself.
success = _compilationRecord[library];
if (success || options.codegenOptions.forceCompile) {
var unit = units.first;
var parts = units.skip(1).toList();
_pendingLibraries.add(new LibraryUnit(unit, parts));
}
// Return tentative success status.
if (!success) _failure = true;
return success;
}
void _copyDartRuntime() {
if (_sdkCopied) return;
_sdkCopied = true;
for (var file in defaultRuntimeFiles) {
var input = new File(path.join(options.runtimeDir, file));
var output = new File(path.join(_runtimeOutputDir, file));
if (output.existsSync() &&
output.lastModifiedSync() == input.lastModifiedSync()) {
continue;
}
fileSystem.copySync(input.path, output.path);
}
}
void _compileHtml(Source source, CompilationNotifier notifier) {
// TODO(jmesserly): reuse DartScriptsTask instead of copy/paste.
var contents = context.getContents(source);
var document = html.parse(contents.data, generateSpans: true);
var scripts = document.querySelectorAll('script[type="application/dart"]');
var loadedLibs = new LinkedHashSet<Uri>();
var htmlOutDir = path.dirname(getOutputPath(source.uri));
for (var script in scripts) {
Source scriptSource = null;
var srcAttr = script.attributes['src'];
if (srcAttr == null) {
if (script.hasContent()) {
var fragments = <ScriptFragment>[];
for (var node in script.nodes) {
if (node is html.Text) {
var start = node.sourceSpan.start;
fragments.add(new ScriptFragment(
start.offset, start.line, start.column, node.data));
}
}
scriptSource = new DartScript(source, fragments);
}
} else if (AnalysisEngine.isDartFileName(srcAttr)) {
scriptSource = context.sourceFactory.resolveUri(source, srcAttr);
}
if (scriptSource != null) {
var lib = context.computeLibraryElement(scriptSource);
_compileLibrary(lib, notifier);
script.replaceWith(_linkLibraries(lib, loadedLibs, from: htmlOutDir));
}
}
fileSystem.writeAsStringSync(
getOutputPath(source.uri), document.outerHtml + '\n');
}
html.DocumentFragment _linkLibraries(
LibraryElement mainLib, LinkedHashSet<Uri> loaded,
{String from}) {
assert(from != null);
var alreadyLoaded = loaded.length;
_collectLibraries(mainLib, loaded);
var newLibs = loaded.skip(alreadyLoaded);
var df = new html.DocumentFragment();
for (var uri in newLibs) {
if (uri.scheme == 'dart') {
if (uri.path == 'core') {
// TODO(jmesserly): it would be nice to not special case these.
for (var file in defaultRuntimeFiles) {
file = path.join(_runtimeOutputDir, file);
df.append(
html_codegen.libraryInclude(path.relative(file, from: from)));
}
}
} else {
var file = path.join(outputDir, getModulePath(uri));
df.append(html_codegen.libraryInclude(path.relative(file, from: from)));
}
}
df.append(html_codegen.invokeMain(getModuleName(mainLib.source.uri)));
return df;
}
void _collectLibraries(LibraryElement lib, LinkedHashSet<Uri> loaded) {
var uri = lib.source.uri;
if (!loaded.add(uri)) return;
_collectLibraries(_dartCore, loaded);
for (var l in lib.imports) _collectLibraries(l.importedLibrary, loaded);
for (var l in lib.exports) _collectLibraries(l.exportedLibrary, loaded);
// Move the item to the end of the list.
loaded.remove(uri);
loaded.add(uri);
}
}
abstract class AbstractCompiler {
final CompilerOptions options;
final AnalysisContext context;
final AnalysisErrorListener reporter;
final FileSystem fileSystem;
AbstractCompiler(this.context, this.options,
[AnalysisErrorListener listener, this.fileSystem = const FileSystem()])
: reporter = listener ?? AnalysisErrorListener.NULL_LISTENER;
String get outputDir => options.codegenOptions.outputDir;
Uri stringToUri(String uriString) {
var uri = uriString.startsWith('dart:') || uriString.startsWith('package:')
? Uri.parse(uriString)
: new Uri.file(path.absolute(uriString));
return uri;
}
/// Directory presumed to be the common prefix for all input file:// URIs.
/// Used when computing output paths.
///
/// For example:
/// dartdevc -o out foo/a.dart bar/b.dart
///
/// Will produce:
/// out/foo/a.dart
/// out/bar/b.dart
///
/// This is only used if at least one of [options.codegenOptions.inputs] is
/// a file URI.
// TODO(jmesserly): do we need an option for this?
// Other ideas: we could look up and see what package the file is in, treat
// that as a base path. We could also use the current working directory as
// the base.
String get inputBaseDir {
if (_inputBaseDir == null) {
List<String> common = null;
for (var uri in options.inputs.map(stringToUri)) {
if (uri.scheme != 'file') continue;
var segments = path.split(path.dirname(uri.path));
if (common == null) {
common = segments;
} else {
int len = math.min(common.length, segments.length);
while (len > 0 && common[len - 1] != segments[len - 1]) {
len--;
}
common.length = len;
}
}
_inputBaseDir = common == null ? '' : path.joinAll(common);
}
return _inputBaseDir;
}
String _inputBaseDir;
String getOutputPath(Uri uri) => path.join(outputDir, getModulePath(uri));
/// Like [getModuleName] but includes the file extension, either .js or .html.
String getModulePath(Uri uri) {
var ext = path.extension(uri.path);
if (ext == '.dart' || ext == '' && uri.scheme == 'dart') ext = '.js';
return getModuleName(uri) + ext;
}
/// Gets the module name, without extension. For example:
///
/// * dart:core -> dart/core
/// * file:foo/bar/baz.dart -> foo/bar/baz
/// * package:qux/qux.dart -> qux/qux
///
/// For file: URLs this will also make them relative to [inputBaseDir].
// TODO(jmesserly): we need to figure out a way to keep package and file URLs
// from conflicting.
String getModuleName(Uri uri) {
var filepath = path.withoutExtension(uri.path);
if (uri.scheme == 'dart') {
return 'dart/$filepath';
} else if (uri.scheme == 'file') {
return path.relative(filepath, from: inputBaseDir);
} else {
assert(uri.scheme == 'package');
// filepath is good here, we want the output to start with a directory
// matching the package name.
return filepath;
}
}
/// Log any errors encountered when resolving [source] and return whether any
/// errors were found.
bool computeErrors(Source source) {
AnalysisContext errorContext = context;
// TODO(jmesserly): should this be a fix somewhere in analyzer?
// otherwise we fail to find the parts.
if (source.uri.scheme == 'dart') {
errorContext = context.sourceFactory.dartSdk.context;
}
List<AnalysisError> errors = errorContext.computeErrors(source);
bool failure = false;
for (var error in errors) {
ErrorCode code = error.errorCode;
// Always skip TODOs.
if (code.type == ErrorType.TODO) continue;
// TODO(jmesserly): for now, treat DDC errors as having a different
// error level from Analayzer ones.
if (isStrongModeError(code)) {
reporter.onError(error);
if (code.errorSeverity == ErrorSeverity.ERROR) {
failure = true;
}
} else if (code.errorSeverity.ordinal >= ErrorSeverity.WARNING.ordinal) {
// All analyzer warnings or errors are errors for DDC.
failure = true;
reporter.onError(error);
} else {
// Skip hints for now.
}
}
return failure;
}
}
AnalysisErrorListener createErrorReporter(
AnalysisContext context, CompilerOptions options) {
return options.htmlReport
? new HtmlReporter(context)
: options.dumpInfo
? new SummaryReporter(context, options.logLevel)
: new LogReporter(context, useColors: options.useColors);
}
// TODO(jmesserly): find a better home for these.
/// Curated order to minimize lazy classes needed by dart:core and its
/// transitive SDK imports.
final corelibOrder = [
'dart:core',
'dart:collection',
'dart:_internal',
'dart:math',
'dart:_interceptors',
'dart:async',
'dart:_foreign_helper',
'dart:_js_embedded_names',
'dart:_js_helper',
'dart:isolate',
'dart:typed_data',
'dart:_native_typed_data',
'dart:_isolate_helper',
'dart:_js_primitives',
'dart:convert',
// TODO(jmesserly): these are not part of corelib library cycle, and shouldn't
// be listed here. Instead, their source should be copied on demand if they
// are actually used by the application.
'dart:mirrors',
'dart:_js_mirrors',
'dart:js',
'dart:_metadata',
'dart:html',
'dart:html_common',
'dart:_debugger'
// _foreign_helper is not included, as it only defines the JS builtin that
// the compiler handles at compile time.
].map(Uri.parse).toList();
/// Runtime files added to all applications when running the compiler in the
/// command line.
final defaultRuntimeFiles = () {
String coreToFile(Uri uri) => uri.toString().replaceAll(':', '/') + '.js';
var files = [
'harmony_feature_check.js',
'dart_library.js',
'dart/_runtime.js',
];
files.addAll(corelibOrder.map(coreToFile));
return files;
}();
final _log = new Logger('dev_compiler.src.compiler');