| // 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/analysis_options.dart'; | |
| import 'package:analyzer/src/analysis_options/analysis_options_parser.dart'; | |
| import 'package:analyzer/src/dart/analysis/analysis_context_collection.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/manifest/manifest_validator.dart'; | |
| import 'package:analyzer/src/pubspec/pubspec_validator.dart'; | |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; | |
| 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; | |
| 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(); | |
| /// Create a new Driver instance. | |
| new({@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, | |
| 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; | |
| // 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; | |
| final gotErrorsFor = <String>{}; | |
| 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 contextRoot = | |
| analysisDriver.currentSession.analysisContext.contextRoot; | |
| var package = contextRoot.workspace.findPackageFor(file.path); | |
| var sdkVersionConstraint = (package is PubPackage) | |
| ? package.sdkVersionConstraint | |
| : null; | |
| var parseResult = AnalysisOptionsParseSession().parse( | |
| sourceFactory: analysisDriver.sourceFactory, | |
| contextRoot: contextRoot.root, | |
| file: file, | |
| sdkVersionConstraint: sdkVersionConstraint, | |
| ); | |
| var fileContent = parseResult.content; | |
| if (fileContent == null) continue; | |
| var lineInfo = fileContent.lineInfo; | |
| var errors = parseResult.diagnostics; | |
| var analysisOptions = parseResult.analysisOptions; | |
| await formatter.formatErrors([ | |
| ErrorsResultImpl( | |
| session: analysisDriver.currentSession, | |
| file: file, | |
| content: fileContent.text, | |
| uri: pathContext.toUri(path), | |
| lineInfo: lineInfo, | |
| isLibrary: true, | |
| isPart: false, | |
| diagnostics: errors, | |
| analysisOptions: analysisOptions, | |
| ), | |
| ]); | |
| for (var error in errors) { | |
| var severity = determineProcessedSeverity(error, 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, | |
| 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, | |
| 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, formatter, gotErrorsFor); | |
| 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 && | |
| analysisContext!.contextRoot.isAnalyzed(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, | |
| ErrorFormatter formatter, | |
| Set<String> gotErrorsFor, | |
| ) { | |
| var startTime = currentTimeMillis; | |
| final analysisDriver = this.analysisDriver!; | |
| var analysisOptions = analysisDriver.getAnalysisOptionsForFile( | |
| file.resource, | |
| ); | |
| var analyzer = AnalyzerImpl( | |
| analysisOptions, | |
| analysisDriver, | |
| file, | |
| stats, | |
| startTime, | |
| gotErrorsFor, | |
| ); | |
| 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; | |
| new(this._resourceProvider) | |
| : _fileContentCache = FileContentCache(_resourceProvider); | |
| DriverBasedAnalysisContext? get analysisContext { | |
| return _analysisContext; | |
| } | |
| AnalysisDriver get analysisDriver { | |
| return _analysisContext!.driver; | |
| } | |
| 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, | |
| packageConfigFile: _commandLineOptions!.defaultPackagesPath, | |
| resourceProvider: _resourceProvider, | |
| sdkPath: _commandLineOptions!.dartSdkPath, | |
| configureAnalysisOptionsBuilder: _configureAnalysisOptionsBuilder, | |
| fileContentCache: _fileContentCache, | |
| withFineDependencies: true, | |
| ); | |
| _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 _configureAnalysisOptionsBuilder({ | |
| required AnalysisOptionsBuilder analysisOptionsBuilder, | |
| }) { | |
| _commandLineOptions!.configureAnalysisOptionsBuilder( | |
| analysisOptionsBuilder, | |
| ); | |
| } | |
| void _setContextForPath(String path) { | |
| _analysisContext = _collection!.contextFor(path); | |
| } | |
| } |