blob: ca8658de0af10ae1f8fe058bfb25b6e103640785 [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.
import 'dart:io' as io;
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/sdk/build_sdk_summary.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/analysis_options/options_file_validator.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/analysis_options.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/dart/analysis/results.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/manifest/manifest_validator.dart';
import 'package:analyzer/src/pubspec/pubspec_validator.dart';
import 'package:analyzer/src/source/path_filter.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/yaml.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer_cli/src/analyzer_impl.dart';
import 'package:analyzer_cli/src/batch_mode.dart';
import 'package:analyzer_cli/src/error_formatter.dart';
import 'package:analyzer_cli/src/error_severity.dart';
import 'package:analyzer_cli/src/options.dart';
import 'package:analyzer_cli/src/perf_report.dart';
import 'package:analyzer_cli/starter.dart' show CommandLineStarter;
import 'package:linter/src/rules.dart' as linter;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
/// Shared IO sink for standard error reporting.
StringSink errorSink = io.stderr;
/// Shared IO sink for standard out reporting.
StringSink outSink = io.stdout;
/// Test this option map to see if it specifies lint rules.
bool containsLintRuleEntry(YamlMap options) {
var linterNode = options.valueAt('linter');
return linterNode is YamlMap && linterNode.valueAt('rules') != null;
}
class Driver implements CommandLineStarter {
static final ByteStore analysisDriverMemoryByteStore = MemoryByteStore();
bool _isStarted = false;
late _AnalysisContextProvider _analysisContextProvider;
DriverBasedAnalysisContext? analysisContext;
/// The driver that was most recently created by a call to [_analyzeAll].
@visibleForTesting
AnalysisDriver? analysisDriver;
/// The total number of source files loaded by an AnalysisContext.
int _analyzedFileCount = 0;
/// The resource provider used to access the file system.
final ResourceProvider resourceProvider = PhysicalResourceProvider.INSTANCE;
/// Collected analysis statistics.
final AnalysisStats stats = AnalysisStats();
/// The [PathFilter] for excluded files with wildcards, etc.
late PathFilter pathFilter;
/// Create a new Driver instance.
Driver({@Deprecated('This parameter has no effect') bool isTesting = false});
/// Converts the given [filePath] into absolute and normalized.
String normalizePath(String filePath) {
filePath = filePath.trim();
filePath = resourceProvider.pathContext.absolute(filePath);
filePath = resourceProvider.pathContext.normalize(filePath);
return filePath;
}
@override
Future<void> start(List<String> arguments) async {
if (_isStarted) {
throw StateError('start() can only be called once');
}
_isStarted = true;
var startTime = DateTime.now().millisecondsSinceEpoch;
linter.registerLintRules();
// Parse commandline options.
var options = CommandLineOptions.parse(resourceProvider, arguments)!;
_analysisContextProvider = _AnalysisContextProvider(resourceProvider);
// Do analysis.
if (options.batchMode) {
var batchRunner = BatchRunner(outSink, errorSink);
batchRunner.runAsBatch(arguments, (List<String> args) async {
var options = CommandLineOptions.parse(resourceProvider, args)!;
return await _analyzeAll(options);
});
} else {
var severity = await _analyzeAll(options);
// Propagate issues to the exit code.
if (_shouldBeFatal(severity)) {
io.exitCode = severity.ordinal;
}
}
// When training a snapshot, in addition to training regular analysis
// (above), we train build mode as well.
if (options.trainSnapshot) {
// TODO(devoncarew): Iterate on this training to make it more
// representative of what we see internally; call into _buildModeAnalyze()
// with some appropriate options.
print('\nGenerating strong mode summary...');
final stopwatch = Stopwatch()..start();
for (var i = 0; i < 3; i++) {
await buildSdkSummary(
resourceProvider: PhysicalResourceProvider.INSTANCE,
sdkPath: options.dartSdkPath!,
);
}
print('Done in ${stopwatch.elapsedMilliseconds} ms.');
}
if (analysisDriver != null) {
_analyzedFileCount += analysisDriver!.knownFiles.length;
}
await _analysisContextProvider.dispose();
if (options.perfReport != null) {
var json = makePerfReport(
startTime,
currentTimeMillis,
options,
_analyzedFileCount,
stats,
);
io.File(options.perfReport!).writeAsStringSync(json);
}
}
/// Perform analysis according to the given [options].
Future<DiagnosticSeverity> _analyzeAll(CommandLineOptions options) async {
if (!options.jsonFormat && !options.machineFormat) {
var fileNames =
options.sourceFiles.map((String file) {
file = path.normalize(file);
if (file == '.') {
file = path.basename(path.current);
} else if (file == '..') {
file = path.basename(path.normalize(path.absolute(file)));
}
return file;
}).toList();
outSink.writeln("Analyzing ${fileNames.join(', ')}...");
}
_verifyAnalysisOptionsFileExists(options);
// These are used to do part file analysis across sources.
var dartFiles = <String>{};
final analyzedFiles = <FileState>{};
final partFiles = <FileState>{};
// Note: This references analysisDriver via closure, so it will change over
// time during the following analysis.
SeverityProcessor defaultSeverityProcessor;
defaultSeverityProcessor = (Diagnostic diagnostic) {
var filePath = diagnostic.source.fullName;
var file = analysisDriver!.resourceProvider.getFile(filePath);
return determineProcessedSeverity(
diagnostic,
options,
analysisDriver!.getAnalysisOptionsForFile(file),
);
};
// We currently print out to stderr to ensure that when in batch mode we
// print to stderr, this is because the prints from batch are made to
// stderr. The reason that options.shouldBatch isn't used is because when
// the argument flags are constructed in BatchRunner and passed in from
// batch mode which removes the batch flag to prevent the "cannot have the
// batch flag and source file" error message.
ErrorFormatter formatter;
if (options.jsonFormat) {
formatter = JsonErrorFormatter(
outSink,
options,
stats,
severityProcessor: defaultSeverityProcessor,
);
} else if (options.machineFormat) {
// The older machine format emits to stderr (instead of stdout) for legacy
// reasons.
formatter = MachineErrorFormatter(
errorSink,
options,
stats,
severityProcessor: defaultSeverityProcessor,
);
} else {
formatter = HumanErrorFormatter(
outSink,
options,
stats,
severityProcessor: defaultSeverityProcessor,
);
}
var allResult = DiagnosticSeverity.NONE;
void reportPartError(String partPath) {
errorSink.writeln('$partPath is a part and cannot be analyzed.');
errorSink.writeln('Please pass in a library that contains this part.');
io.exitCode = DiagnosticSeverity.ERROR.ordinal;
allResult = allResult.max(DiagnosticSeverity.ERROR);
}
var pathList = options.sourceFiles.map(normalizePath).toList();
_analysisContextProvider.setCommandLineOptions(options, pathList);
for (var sourcePath in pathList) {
_analysisContextProvider.configureForPath(sourcePath);
analysisContext = _analysisContextProvider.analysisContext;
final analysisDriver =
this.analysisDriver = _analysisContextProvider.analysisDriver;
pathFilter = _analysisContextProvider.pathFilter;
// Add all the files to be analyzed en masse to the context. Skip any
// files that were added earlier (whether explicitly or implicitly) to
// avoid causing those files to be unnecessarily re-read.
var filesToAnalyze = <String>{};
// Collect files for analysis.
// Note that these files will all be analyzed in the same context.
// This should be updated when the ContextManager re-work is complete
// (See: https://github.com/dart-lang/sdk/issues/24133)
var files = _collectFiles(sourcePath);
if (files.isEmpty) {
errorSink.writeln('No dart files found at: $sourcePath');
io.exitCode = DiagnosticSeverity.ERROR.ordinal;
return DiagnosticSeverity.ERROR;
}
for (var file in files) {
filesToAnalyze.add(file.absolute.path);
}
// Analyze the libraries.
var pathContext = resourceProvider.pathContext;
for (var path in filesToAnalyze) {
if (file_paths.isAnalysisOptionsYaml(pathContext, path)) {
var fileResult = analysisDriver.currentSession.getFile(path);
if (fileResult is! FileResult) continue;
var file = fileResult.file;
var content = file.readAsStringSync();
var lineInfo = LineInfo.fromContent(content);
var contextRoot =
analysisDriver.currentSession.analysisContext.contextRoot;
var package = contextRoot.workspace.findPackageFor(file.path);
var sdkVersionConstraint =
(package is PubPackage) ? package.sdkVersionConstraint : null;
var errors = analyzeAnalysisOptions(
FileSource(file),
content,
analysisDriver.sourceFactory,
contextRoot.root.path,
sdkVersionConstraint,
);
var analysisOptions = fileResult.analysisOptions;
await formatter.formatErrors([
ErrorsResultImpl(
session: analysisDriver.currentSession,
file: file,
content: content,
uri: pathContext.toUri(path),
lineInfo: lineInfo,
isLibrary: true,
isPart: false,
diagnostics: errors,
analysisOptions: analysisOptions,
),
]);
for (var error in errors) {
var severity = determineProcessedSeverity(
error,
options,
analysisOptions,
);
if (severity != null) {
allResult = allResult.max(severity);
}
}
} else if (file_paths.isPubspecYaml(pathContext, path)) {
var diagnostics = <Diagnostic>[];
try {
var file = resourceProvider.getFile(path);
var analysisOptions = analysisDriver.currentSession.analysisContext
.getAnalysisOptionsForFile(file);
var content = file.readAsStringSync();
var node = loadYamlNode(content, sourceUrl: file.toUri());
if (node is YamlMap) {
diagnostics.addAll(
validatePubspec(
contents: node,
source: FileSource(file),
provider: resourceProvider,
analysisOptions: analysisOptions,
),
);
}
if (diagnostics.isNotEmpty) {
for (var error in diagnostics) {
var severity =
determineProcessedSeverity(
error,
options,
analysisOptions,
)!;
allResult = allResult.max(severity);
}
var lineInfo = LineInfo.fromContent(content);
await formatter.formatErrors([
ErrorsResultImpl(
session: analysisDriver.currentSession,
file: file,
content: content,
uri: pathContext.toUri(path),
lineInfo: lineInfo,
isLibrary: true,
isPart: false,
diagnostics: diagnostics,
analysisOptions: analysisOptions,
),
]);
}
} catch (exception) {
// If the file cannot be analyzed, ignore it.
}
} else if (file_paths.isAndroidManifestXml(pathContext, path)) {
try {
var file = resourceProvider.getFile(path);
var analysisOptions = analysisDriver.getAnalysisOptionsForFile(
file,
);
var content = file.readAsStringSync();
var source = FileSource(file);
var validator = ManifestValidator(source);
var lineInfo = LineInfo.fromContent(content);
var errors = validator.validate(
content,
analysisOptions.chromeOsManifestChecks,
);
await formatter.formatErrors([
ErrorsResultImpl(
session: analysisDriver.currentSession,
file: file,
content: content,
uri: pathContext.toUri(path),
lineInfo: lineInfo,
isLibrary: true,
isPart: false,
diagnostics: errors,
analysisOptions: analysisOptions,
),
]);
for (var error in errors) {
var severity =
determineProcessedSeverity(error, options, analysisOptions)!;
allResult = allResult.max(severity);
}
} catch (exception) {
// If the file cannot be analyzed, ignore it.
}
} else {
dartFiles.add(path);
var file = analysisDriver.fsState.getFileForPath(path);
final kind = file.kind;
if (kind is LibraryFileKind) {
var status = await _runAnalyzer(file, options, formatter);
allResult = allResult.max(status);
analyzedFiles.addAll(kind.files);
} else if (kind is PartFileKind) {
partFiles.add(file);
}
}
}
}
// We are done analyzing this batch of files.
// The next batch should not be affected by a previous batch.
// E.g. the same parts in both batches, but with different libraries.
for (var path in dartFiles) {
analysisDriver!.removeFile(path);
}
// Any dangling parts still in this list were definitely dangling.
for (var partFile in partFiles) {
if (!analyzedFiles.contains(partFile)) {
reportPartError(partFile.path);
}
}
formatter.flush();
if (!options.machineFormat && !options.jsonFormat) {
stats.print(outSink);
}
return allResult;
}
/// Collect all analyzable files at [filePath], recursively if it's a
/// directory, ignoring links.
Iterable<io.File> _collectFiles(String filePath) {
var files = <io.File>[];
var file = io.File(filePath);
if (file.existsSync()) {
files.add(file);
} else {
var directory = io.Directory(filePath);
if (directory.existsSync()) {
var pathContext = resourceProvider.pathContext;
for (var entry in directory.listSync(
recursive: true,
followLinks: false,
)) {
var relative = path.relative(entry.path, from: directory.path);
if ((file_paths.isDart(pathContext, entry.path) ||
file_paths.isAndroidManifestXml(pathContext, entry.path)) &&
entry is io.File &&
!pathFilter.ignored(entry.path) &&
!_isInHiddenDir(relative)) {
files.add(entry);
}
}
}
}
return files;
}
/// Returns `true` if this relative path is a hidden directory.
bool _isInHiddenDir(String relative) =>
path.split(relative).any((part) => part.startsWith('.'));
/// Analyze a single source.
Future<DiagnosticSeverity> _runAnalyzer(
FileState file,
CommandLineOptions options,
ErrorFormatter formatter,
) {
var startTime = currentTimeMillis;
final analysisDriver = this.analysisDriver!;
var analysisOptions = analysisDriver.getAnalysisOptionsForFile(
file.resource,
);
var analyzer = AnalyzerImpl(
analysisOptions,
analysisDriver,
file,
options,
stats,
startTime,
);
return analyzer.analyze(formatter);
}
bool _shouldBeFatal(DiagnosticSeverity severity) =>
severity == DiagnosticSeverity.ERROR;
void _verifyAnalysisOptionsFileExists(CommandLineOptions options) {
var path = options.defaultAnalysisOptionsPath;
if (path != null) {
if (!resourceProvider.getFile(path).exists) {
printAndFail(
'Options file not found: $path',
exitCode: DiagnosticSeverity.ERROR.ordinal,
);
}
}
}
static void verbosePrint(String text) {
outSink.writeln(text);
}
/// Return whether the [newOptions] are equal to the [previous].
static bool _equalCommandLineOptions(
CommandLineOptions? previous,
CommandLineOptions newOptions,
) {
return previous != null &&
newOptions.defaultPackagesPath == previous.defaultPackagesPath &&
_equalMaps(newOptions.declaredVariables, previous.declaredVariables) &&
newOptions.log == previous.log &&
newOptions.disableCacheFlushing == previous.disableCacheFlushing &&
_equalLists(newOptions.enabledExperiments, previous.enabledExperiments);
}
/// Perform a deep comparison of two string lists.
static bool _equalLists(List<String> l1, List<String> l2) {
if (l1.length != l2.length) {
return false;
}
for (var i = 0; i < l1.length; i++) {
if (l1[i] != l2[i]) {
return false;
}
}
return true;
}
/// Perform a deep comparison of two string maps.
static bool _equalMaps(Map<String, String> m1, Map<String, String> m2) {
if (m1.length != m2.length) {
return false;
}
for (var key in m1.keys) {
if (!m2.containsKey(key) || m1[key] != m2[key]) {
return false;
}
}
return true;
}
}
class _AnalysisContextProvider {
final ResourceProvider _resourceProvider;
final FileContentCache _fileContentCache;
CommandLineOptions? _commandLineOptions;
late List<String> _pathList;
final List<AnalysisContextCollectionImpl> _toDispose = [];
final Map<Folder, DriverBasedAnalysisContext?> _folderContexts = {};
AnalysisContextCollectionImpl? _collection;
DriverBasedAnalysisContext? _analysisContext;
_AnalysisContextProvider(this._resourceProvider)
: _fileContentCache = FileContentCache(_resourceProvider);
DriverBasedAnalysisContext? get analysisContext {
return _analysisContext;
}
AnalysisDriver get analysisDriver {
return _analysisContext!.driver;
}
// TODO(scheglov): Use analyzedFiles()
PathFilter get pathFilter {
var contextRoot = analysisContext!.contextRoot;
var optionsFile = contextRoot.optionsFile;
// If there is no options file, there can be no excludes.
if (optionsFile == null) {
return PathFilter(contextRoot.root.path, contextRoot.root.path, []);
}
// Exclude patterns are relative to the directory with the options file.
return PathFilter(
contextRoot.root.path,
optionsFile.parent.path,
analysisContext!.getAnalysisOptionsForFile(optionsFile).excludePatterns,
);
}
void configureForPath(String path) {
var folder = _resourceProvider.getFolder(path);
if (!folder.exists) {
folder = _resourceProvider.getFile(path).parent;
}
// In batch mode we are given separate file paths to analyze.
// All files of a folder have the same configuration.
// So, reuse the corresponding analysis context.
_analysisContext = _folderContexts[folder];
if (_analysisContext != null) {
return;
}
if (_collection != null) {
try {
_setContextForPath(path);
return;
} on StateError {
// The previous collection cannot analyze the path.
_collection = null;
}
}
_collection = AnalysisContextCollectionImpl(
byteStore: Driver.analysisDriverMemoryByteStore,
includedPaths: _pathList,
optionsFile: _commandLineOptions!.defaultAnalysisOptionsPath,
packagesFile: _commandLineOptions!.defaultPackagesPath,
resourceProvider: _resourceProvider,
sdkPath: _commandLineOptions!.dartSdkPath,
updateAnalysisOptions3: _updateAnalysisOptions,
fileContentCache: _fileContentCache,
);
_toDispose.add(_collection!);
_setContextForPath(path);
_folderContexts[folder] = _analysisContext;
}
Future<void> dispose() async {
_collection = null;
var toDispose = _toDispose.toList();
_toDispose.clear();
for (var collection in toDispose) {
await collection.dispose();
}
}
void setCommandLineOptions(
CommandLineOptions options,
List<String> pathList,
) {
if (!Driver._equalCommandLineOptions(_commandLineOptions, options)) {
_folderContexts.clear();
_collection = null;
_analysisContext = null;
}
_commandLineOptions = options;
_pathList = pathList;
}
void _setContextForPath(String path) {
_analysisContext = _collection!.contextFor(path);
}
void _updateAnalysisOptions({
required AnalysisOptionsImpl analysisOptions,
required DartSdk sdk,
}) {
_commandLineOptions!.updateAnalysisOptions(analysisOptions);
}
}