blob: 22562473d38804b0136364c79617a8efd2a4848e [file] [log] [blame]
// 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.
library analyzer_cli.src.build_mode;
import 'dart:convert';
import 'dart:core' hide Resource;
import 'dart:io' as io;
import 'package:analyzer/dart/ast/ast.dart' show CompilationUnit;
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/error.dart';
import 'package:analyzer/src/generated/java_io.dart';
import 'package:analyzer/src/generated/sdk_io.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/source_io.dart';
import 'package:analyzer/src/summary/format.dart';
import 'package:analyzer/src/summary/idl.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/summary/prelink.dart';
import 'package:analyzer/src/summary/summarize_ast.dart';
import 'package:analyzer/src/summary/summarize_elements.dart';
import 'package:analyzer/task/dart.dart';
import 'package:analyzer_cli/src/analyzer_impl.dart';
import 'package:analyzer_cli/src/driver.dart';
import 'package:analyzer_cli/src/error_formatter.dart';
import 'package:analyzer_cli/src/options.dart';
/**
* Analyzer used when the "--build-mode" option is supplied.
*/
class BuildMode {
final CommandLineOptions options;
final AnalysisStats stats;
final ResourceProvider resourceProvider = PhysicalResourceProvider.INSTANCE;
SummaryDataStore summaryDataStore;
InternalAnalysisContext context;
Map<Uri, JavaFile> uriToFileMap;
final List<Source> explicitSources = <Source>[];
PackageBundleAssembler assembler = new PackageBundleAssembler();
final Set<Source> processedSources = new Set<Source>();
final Map<Uri, UnlinkedUnit> uriToUnit = <Uri, UnlinkedUnit>{};
BuildMode(this.options, this.stats);
/**
* Perform package analysis according to the given [options].
*/
ErrorSeverity analyze() {
// Write initial progress message.
if (!options.machineFormat) {
outSink.writeln("Analyzing sources ${options.sourceFiles}...");
}
// Create the URI to file map.
uriToFileMap = _createUriToFileMap(options.sourceFiles);
if (uriToFileMap == null) {
io.exitCode = ErrorSeverity.ERROR.ordinal;
return ErrorSeverity.ERROR;
}
// Prepare the analysis context.
_createContext();
// Add sources.
ChangeSet changeSet = new ChangeSet();
for (Uri uri in uriToFileMap.keys) {
JavaFile file = uriToFileMap[uri];
if (!file.exists()) {
errorSink.writeln('File not found: ${file.getPath()}');
io.exitCode = ErrorSeverity.ERROR.ordinal;
return ErrorSeverity.ERROR;
}
Source source = new FileBasedSource(file, uri);
explicitSources.add(source);
changeSet.addedSource(source);
}
context.applyChanges(changeSet);
if (!options.buildSummaryOnly) {
// Perform full analysis.
while (true) {
AnalysisResult analysisResult = context.performAnalysisTask();
if (!analysisResult.hasMoreWork) {
break;
}
}
}
// Write summary.
if (options.buildSummaryOutput != null) {
for (Source source in explicitSources) {
if (context.computeKindOf(source) == SourceKind.LIBRARY) {
if (options.buildSummaryFallback) {
assembler.addFallbackLibrary(source);
} else if (options.buildSummaryOnlyAst) {
_serializeAstBasedSummary(source);
} else {
LibraryElement libraryElement =
context.computeLibraryElement(source);
assembler.serializeLibraryElement(libraryElement);
}
}
if (options.buildSummaryFallback) {
assembler.addFallbackUnit(source);
}
}
// Write the whole package bundle.
PackageBundleBuilder sdkBundle = assembler.assemble();
if (options.buildSummaryExcludeInformative) {
sdkBundle.flushInformative();
sdkBundle.unlinkedUnitHashes = null;
}
io.File file = new io.File(options.buildSummaryOutput);
file.writeAsBytesSync(sdkBundle.toBuffer(), mode: io.FileMode.WRITE_ONLY);
}
if (options.buildSummaryOnly) {
return ErrorSeverity.NONE;
} else {
// Process errors.
_printErrors(outputPath: options.buildAnalysisOutput);
return _computeMaxSeverity();
}
}
ErrorSeverity _computeMaxSeverity() {
ErrorSeverity maxSeverity = ErrorSeverity.NONE;
if (!options.buildSuppressExitCode) {
for (Source source in explicitSources) {
AnalysisErrorInfo errorInfo = context.getErrors(source);
for (AnalysisError error in errorInfo.errors) {
ProcessedSeverity processedSeverity =
AnalyzerImpl.processError(error, options, context);
if (processedSeverity != null) {
maxSeverity = maxSeverity.max(processedSeverity.severity);
}
}
}
}
return maxSeverity;
}
void _createContext() {
DirectoryBasedDartSdk sdk =
new DirectoryBasedDartSdk(new JavaFile(options.dartSdkPath));
sdk.analysisOptions =
Driver.createAnalysisOptionsForCommandLineOptions(options);
sdk.useSummary = true;
// Read the summaries.
summaryDataStore = new SummaryDataStore(options.buildSummaryInputs);
// In AST mode include SDK bundle to avoid parsing SDK sources.
if (options.buildSummaryOnlyAst) {
summaryDataStore.addBundle(null, sdk.getSummarySdkBundle());
}
// Create the context.
context = AnalysisEngine.instance.createAnalysisContext();
context.sourceFactory = new SourceFactory(<UriResolver>[
new DartUriResolver(sdk),
new InSummaryPackageUriResolver(summaryDataStore),
new ExplicitSourceResolver(uriToFileMap)
]);
// Set context options.
Driver.setAnalysisContextOptions(context, options,
(AnalysisOptionsImpl contextOptions) {
if (options.buildSummaryOnlyDiet) {
contextOptions.analyzeFunctionBodies = false;
}
});
// Configure using summaries.
context.typeProvider = sdk.context.typeProvider;
context.resultProvider =
new InputPackagesResultProvider(context, summaryDataStore);
}
/**
* Print errors for all explicit sources. If [outputPath] is supplied, output
* is sent to a new file at that path.
*/
void _printErrors({String outputPath}) {
StringBuffer buffer = new StringBuffer();
ErrorFormatter formatter = new ErrorFormatter(
buffer,
options,
stats,
(AnalysisError error) =>
AnalyzerImpl.processError(error, options, context));
for (Source source in explicitSources) {
AnalysisErrorInfo errorInfo = context.getErrors(source);
formatter.formatErrors([errorInfo]);
}
if (!options.machineFormat) {
stats.print(buffer);
}
if (outputPath == null) {
StringSink sink = options.machineFormat ? errorSink : outSink;
sink.write(buffer);
} else {
new io.File(outputPath).writeAsStringSync(buffer.toString());
}
}
/**
* Serialize the library with the given [source] into [assembler] using only
* its AST, [UnlinkedUnit]s of input packages and ASTs (via [UnlinkedUnit]s)
* of package sources.
*/
void _serializeAstBasedSummary(Source source) {
Source resolveRelativeUri(String relativeUri) {
Source resolvedSource =
context.sourceFactory.resolveUri(source, relativeUri);
if (resolvedSource == null) {
context.sourceFactory.resolveUri(source, relativeUri);
throw new StateError('Could not resolve $relativeUri in the context of '
'$source (${source.runtimeType})');
}
return resolvedSource;
}
UnlinkedUnit _getUnlinkedUnit(Source source) {
// Maybe an input package contains the source.
{
String uriStr = source.uri.toString();
UnlinkedUnit unlinkedUnit = summaryDataStore.unlinkedMap[uriStr];
if (unlinkedUnit != null) {
return unlinkedUnit;
}
}
// Parse the source and serialize its AST.
return uriToUnit.putIfAbsent(source.uri, () {
CompilationUnit unit = context.computeResult(source, PARSED_UNIT);
UnlinkedUnitBuilder unlinkedUnit = serializeAstUnlinked(unit);
assembler.addUnlinkedUnit(source, unlinkedUnit);
return unlinkedUnit;
});
}
UnlinkedUnit getPart(String relativeUri) {
return _getUnlinkedUnit(resolveRelativeUri(relativeUri));
}
UnlinkedPublicNamespace getImport(String relativeUri) {
return getPart(relativeUri).publicNamespace;
}
UnlinkedUnitBuilder definingUnit = _getUnlinkedUnit(source);
LinkedLibraryBuilder linkedLibrary =
prelink(definingUnit, getPart, getImport);
assembler.addLinkedLibrary(source.uri.toString(), linkedLibrary);
}
/**
* Convert [sourceEntities] (a list of file specifications of the form
* "$uri|$path") to a map from URI to path. If an error occurs, report the
* error and return null.
*/
static Map<Uri, JavaFile> _createUriToFileMap(List<String> sourceEntities) {
Map<Uri, JavaFile> uriToFileMap = <Uri, JavaFile>{};
for (String sourceFile in sourceEntities) {
int pipeIndex = sourceFile.indexOf('|');
if (pipeIndex == -1) {
// TODO(paulberry): add the ability to guess the URI from the path.
errorSink.writeln(
'Illegal input file (must be "\$uri|\$path"): $sourceFile');
return null;
}
Uri uri = Uri.parse(sourceFile.substring(0, pipeIndex));
String path = sourceFile.substring(pipeIndex + 1);
uriToFileMap[uri] = new JavaFile(path);
}
return uriToFileMap;
}
}
/**
* Interface that every worker related data object has.
*/
abstract class WorkDataObject {
/**
* Translate the data in this class into a JSON map.
*/
Map<String, Object> toJson();
}
/**
* Connection between a worker and input / output.
*/
abstract class WorkerConnection {
/**
* Read a new line. Block until a line is read. Return `null` if EOF.
*/
String readLineSync();
/**
* Write the given [json] as a new line to the output.
*/
void writeJson(Map<String, Object> json);
}
/**
* Persistent Bazel worker.
*/
class WorkerLoop {
static const int EXIT_CODE_OK = 0;
static const int EXIT_CODE_ERROR = 15;
final WorkerConnection connection;
final StringBuffer errorBuffer = new StringBuffer();
final StringBuffer outBuffer = new StringBuffer();
WorkerLoop(this.connection);
factory WorkerLoop.std() {
WorkerConnection connection = new _StdWorkerConnection();
return new WorkerLoop(connection);
}
/**
* Performs analysis with given [options].
*/
void analyze(CommandLineOptions options) {
new BuildMode(options, new AnalysisStats()).analyze();
}
/**
* Perform a single loop step. Return `true` if should exit the loop.
*/
bool performSingle() {
try {
WorkRequest request = _readRequest();
if (request == null) {
return true;
}
// Prepare options.
CommandLineOptions options =
CommandLineOptions.parse(request.arguments, (String msg) {
throw new ArgumentError(msg);
});
// Analyze and respond.
analyze(options);
String msg = _getErrorOutputBuffersText();
_writeResponse(new WorkResponse(EXIT_CODE_OK, msg));
} catch (e, st) {
String msg = _getErrorOutputBuffersText();
msg += '$e \n $st';
_writeResponse(new WorkResponse(EXIT_CODE_ERROR, msg));
}
return false;
}
/**
* Run the worker loop.
*/
void run() {
errorSink = errorBuffer;
outSink = outBuffer;
exitHandler = (int exitCode) {
return throw new StateError('Exit called: $exitCode');
};
while (true) {
errorBuffer.clear();
outBuffer.clear();
bool shouldExit = performSingle();
if (shouldExit) {
break;
}
}
}
String _getErrorOutputBuffersText() {
String msg = '';
if (errorBuffer.isNotEmpty) {
msg += errorBuffer.toString() + '\n';
}
if (outBuffer.isNotEmpty) {
msg += outBuffer.toString() + '\n';
}
return msg;
}
/**
* Read a new [WorkRequest]. Return `null` if EOF.
* Throw [ArgumentError] if cannot be parsed.
*/
WorkRequest _readRequest() {
String line = connection.readLineSync();
if (line == null) {
return null;
}
Object json = JSON.decode(line);
if (json is Map) {
return new WorkRequest.fromJson(json);
} else {
throw new ArgumentError('The request line is not a JSON object: $line');
}
}
void _writeResponse(WorkResponse response) {
Map<String, Object> json = response.toJson();
connection.writeJson(json);
}
}
/**
* Input file.
*/
class WorkInput implements WorkDataObject {
final String path;
final List<int> digest;
WorkInput(this.path, this.digest);
factory WorkInput.fromJson(Map<String, Object> json) {
// Parse path.
Object path2 = json['path'];
if (path2 == null) {
throw new ArgumentError('The field "path" is missing.');
}
if (path2 is! String) {
throw new ArgumentError('The field "path" must be a string.');
}
// Parse digest.
List<int> digest = const <int>[];
{
Object digestJson = json['digest'];
if (digestJson != null) {
if (digestJson is List && digestJson.every((e) => e is int)) {
digest = digestJson;
} else {
throw new ArgumentError(
'The field "digest" should be a list of int.');
}
}
}
// OK
return new WorkInput(path2, digest);
}
@override
Map<String, Object> toJson() {
Map<String, Object> json = <String, Object>{};
if (path != null) {
json['path'] = path;
}
if (digest != null) {
json['digest'] = digest;
}
return json;
}
}
/**
* Single work unit that Bazel sends to the worker.
*/
class WorkRequest implements WorkDataObject {
/**
* Command line arguments for this request.
*/
final List<String> arguments;
/**
* Input files that the worker is allowed to read during execution of this
* request.
*/
final List<WorkInput> inputs;
WorkRequest(this.arguments, this.inputs);
factory WorkRequest.fromJson(Map<String, Object> json) {
// Parse arguments.
List<String> arguments = const <String>[];
{
Object argumentsJson = json['arguments'];
if (argumentsJson != null) {
if (argumentsJson is List && argumentsJson.every((e) => e is String)) {
arguments = argumentsJson;
} else {
throw new ArgumentError(
'The field "arguments" should be a list of strings.');
}
}
}
// Parse inputs.
List<WorkInput> inputs = const <WorkInput>[];
{
Object inputsJson = json['inputs'];
if (inputsJson != null) {
if (inputsJson is List &&
inputsJson.every((e) {
return e is Map && e.keys.every((key) => key is String);
})) {
inputs = inputsJson
.map((Map input) => new WorkInput.fromJson(input))
.toList();
} else {
throw new ArgumentError(
'The field "inputs" should be a list of objects.');
}
}
}
// No inputs.
if (arguments.isEmpty && inputs.isEmpty) {
throw new ArgumentError('Both "arguments" and "inputs" cannot be empty.');
}
// OK
return new WorkRequest(arguments, inputs);
}
@override
Map<String, Object> toJson() {
Map<String, Object> json = <String, Object>{};
if (arguments != null) {
json['arguments'] = arguments;
}
if (inputs != null) {
json['inputs'] = inputs.map((input) => input.toJson()).toList();
}
return json;
}
}
/**
* Result that the worker sends back to Bazel when it finished its work on a
* [WorkRequest] message.
*/
class WorkResponse implements WorkDataObject {
final int exitCode;
final String output;
WorkResponse(this.exitCode, this.output);
@override
Map<String, Object> toJson() {
Map<String, Object> json = <String, Object>{};
if (exitCode != null) {
json['exit_code'] = exitCode;
}
if (output != null) {
json['output'] = output;
}
return json;
}
}
/**
* Default implementation of [WorkerConnection] that works with stdio.
*/
class _StdWorkerConnection implements WorkerConnection {
@override
String readLineSync() {
return io.stdin.readLineSync();
}
@override
void writeJson(Map<String, Object> json) {
io.stdout.writeln(JSON.encode(json));
}
}