blob: 136a47cbde3fab1e8a99c653f2deec12536529fb [file] [log] [blame]
// Copyright (c) 2013, 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.
/// A library for compiling Dart code and manipulating analyzer parse trees.
import 'dart:async';
import 'dart:io';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/context_builder.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:path/path.dart' as p;
import 'exceptions.dart';
import 'io.dart';
import 'log.dart' as log;
import 'utils.dart';
/// Returns whether [dart] looks like an entrypoint file.
bool isEntrypoint(CompilationUnit dart) {
// Allow two or fewer arguments so that entrypoints intended for use with
// [spawnUri] get counted.
//
// TODO(nweiz): this misses the case where a Dart file doesn't contain main(),
// but it parts in another file that does.
return dart.declarations.any((node) {
return node is FunctionDeclaration &&
node.name.name == "main" &&
node.functionExpression.parameters.parameters.length <= 2;
});
}
/// Snapshots the Dart executable at [executableUrl] to a snapshot at
/// [snapshotPath].
///
/// If [packagesFile] is passed, it's used to resolve `package:` URIs in the
/// executable. Otherwise, a `packages/` directory or a package spec is inferred
/// from the executable's location.
///
/// If [name] is passed, it is used to describe the executable in logs and error
/// messages.
///
/// When running in Dart 2 mode, this automatically creates a Dart 2-compatible
/// snapshot as well at `$snapshotPath.dart2`.
Future snapshot(Uri executableUrl, String snapshotPath,
{Uri packagesFile, String name}) async {
name = log.bold(name ?? executableUrl.toString());
var dart1Args = [
'--no-preview-dart-2',
'--snapshot=$snapshotPath',
executableUrl.toString()
];
var dart2Path = '$snapshotPath.dart2';
var dart2Args =
isDart2 ? ['--snapshot=$dart2Path', executableUrl.toString()] : null;
if (packagesFile != null) {
dart1Args.insert(0, "--packages=$packagesFile");
// Resolve [packagesFile] in case it's relative to work around sdk#33177.
dart2Args?.insert(0, "--packages=${Uri.base.resolveUri(packagesFile)}");
}
var processes = [runProcess(Platform.executable, dart1Args)];
if (isDart2) processes.add(runProcess(Platform.executable, dart2Args));
var results = await Future.wait(processes);
var failure =
results.firstWhere((result) => !result.success, orElse: () => null);
if (failure == null) {
log.message("Precompiled $name.");
} else {
// Don't leave partial results.
deleteEntry(snapshotPath);
deleteEntry(dart2Path);
throw new ApplicationException(log.yellow("Failed to precompile $name:\n") +
failure.stderr.join('\n'));
}
}
class AnalysisContextManager {
/// The map from a context root directory to to the context.
final Map<String, AnalysisContext> _contexts = {};
/// Ensure that there are analysis contexts for the directory with the
/// given [path]. If any previously added root covers the [path], keep
/// the previously created analysis context.
///
/// This method does not discover analysis roots "up", it only looks down
/// the given [path]. It is expected that the client knows analysis roots
/// in advance. Pub does know, it is the packages it works with.
void createContextsForDirectory(String path) {
_throwIfNotAbsolutePath(path);
// We add all contexts below the given directory.
// So, children contexts must also have been added.
if (_contexts.containsKey(path)) {
return;
}
// Add new contexts for the given path.
var contextLocator = new ContextLocator();
var roots = contextLocator.locateRoots(includedPaths: [path]);
for (var root in roots) {
String contextRootPath = root.root.path;
// If there is already a context for this context root path, keep it.
if (_contexts.containsKey(contextRootPath)) {
continue;
}
var contextBuilder = new ContextBuilder();
var context = contextBuilder.createContext(contextRoot: root);
_contexts[contextRootPath] = context;
}
}
/// Parse the file with the given [path] into AST.
///
/// One of the containing directories must be used to create analysis
/// contexts using [createContextsForDirectory]. Throws [StateError] if
/// this has not been done.
///
/// Throws [AnalyzerErrorGroup] is the file has parsing errors.
CompilationUnit parse(String path) {
var parseResult = _getExistingSession(path).getParsedAstSync(path);
if (parseResult.errors.isNotEmpty) {
throw new AnalyzerErrorGroup(parseResult.errors);
}
return parseResult.unit;
}
/// Return import and export directives in the file with the given [path].
///
/// One of the containing directories must be used to create analysis
/// contexts using [createContextsForDirectory]. Throws [StateError] if
/// this has not been done.
///
/// Throws [AnalyzerErrorGroup] is the file has parsing errors.
List<UriBasedDirective> parseImportsAndExports(String path) {
var unit = parse(path);
var uriDirectives = <UriBasedDirective>[];
for (var directive in unit.directives) {
if (directive is UriBasedDirective) {
uriDirectives.add(directive);
}
}
return uriDirectives;
}
AnalysisSession _getExistingSession(String path) {
_throwIfNotAbsolutePath(path);
for (var context in _contexts.values) {
if (context.contextRoot.isAnalyzed(path)) {
return context.currentSession;
}
}
throw new StateError('Unable to find the context to $path');
}
/// The driver supports only absolute paths, this method is used to validate
/// any input paths to prevent errors later.
void _throwIfNotAbsolutePath(String path) {
if (!p.isAbsolute(path)) {
throw new ArgumentError('Only absolute paths are supported: $path');
}
}
}
/// An error class that contains multiple [AnalysisError]s.
class AnalyzerErrorGroup implements Exception {
final List<AnalysisError> errors;
AnalyzerErrorGroup(this.errors);
String get message => toString();
@override
String toString() => errors.join("\n");
}