| // 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/features.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/src/dart/analysis/analysis_options.dart'; |
| import 'package:analyzer/src/dart/analysis/experiments.dart'; |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; |
| import 'package:analyzer/src/util/sdk.dart'; |
| import 'package:analyzer_cli/src/ansi.dart' as ansi; |
| import 'package:analyzer_cli/src/driver.dart'; |
| import 'package:args/args.dart'; |
| |
| const _analysisOptionsFileOption = 'options'; |
| const _binaryName = 'dartanalyzer'; |
| const _defineVariableOption = 'D'; |
| const _enableExperimentOption = 'enable-experiment'; |
| const _enableInitializingFormalAccessFlag = 'initializing-formal-access'; |
| const _ignoreUnrecognizedFlagsFlag = 'ignore-unrecognized-flags'; |
| const _packagesOption = 'packages'; |
| const _sdkPathOption = 'dart-sdk'; |
| |
| /// Shared exit handler. |
| /// |
| /// *Visible for testing.* |
| ExitHandler exitHandler = io.exit; |
| |
| /// Print the given [message] to stderr and exit with the given [exitCode]. |
| void printAndFail(String message, {int exitCode = 15}) { |
| errorSink.writeln(message); |
| exitHandler(exitCode); |
| } |
| |
| /// Exit handler. |
| /// |
| /// *Visible for testing.* |
| typedef ExitHandler = void Function(int code); |
| |
| /// Analyzer commandline configuration options. |
| class CommandLineOptions { |
| final ArgResults _argResults; |
| |
| /// The file path of the analysis options file that should be used in place of |
| /// any file in the root directory or a parent of the root directory, |
| /// or `null` if the normal lookup mechanism should be used. |
| String? defaultAnalysisOptionsPath; |
| |
| /// The file path of the .packages file that should be used in place of any |
| /// file found using the normal (Package Specification DEP) lookup mechanism, |
| /// or `null` if the normal lookup mechanism should be used. |
| String? defaultPackagesPath; |
| |
| /// A table mapping variable names to values for the declared variables. |
| final Map<String, String> declaredVariables = {}; |
| |
| /// The path to the dart SDK. |
| String? dartSdkPath; |
| |
| /// Whether to disable cache flushing. This option can improve analysis |
| /// speed at the expense of memory usage. It may also be useful for working |
| /// around bugs. |
| final bool disableCacheFlushing; |
| |
| /// Whether to display version information |
| final bool displayVersion; |
| |
| /// Whether to ignore unrecognized flags |
| final bool ignoreUnrecognizedFlags; |
| |
| /// Whether to log additional analysis messages and exceptions |
| final bool log; |
| |
| /// Whether to use 'json' format for error display |
| final bool jsonFormat; |
| |
| /// Whether to use 'machine' format for error display |
| final bool machineFormat; |
| |
| /// The path to a file to write a performance log. |
| /// (Or null if not enabled.) |
| final String? perfReport; |
| |
| /// Batch mode (for unit testing) |
| final bool batchMode; |
| |
| /// The source files to analyze |
| final List<String> sourceFiles; |
| |
| /// Emit output in a verbose mode. |
| final bool verbose; |
| |
| /// Use ANSI color codes for output. |
| final bool color; |
| |
| /// Whether we should analyze the given source for the purposes of training a |
| /// Dart analyzer snapshot. |
| final bool trainSnapshot; |
| |
| /// Initialize options from the given parsed [args]. |
| CommandLineOptions._fromArgs( |
| ResourceProvider resourceProvider, |
| ArgResults args, |
| ) : _argResults = args, |
| dartSdkPath = args.option(_sdkPathOption), |
| disableCacheFlushing = args.flag('disable-cache-flushing'), |
| displayVersion = args.flag('version'), |
| ignoreUnrecognizedFlags = args.flag(_ignoreUnrecognizedFlagsFlag), |
| log = args.flag('log'), |
| jsonFormat = args['format'] == 'json', |
| machineFormat = args['format'] == 'machine', |
| perfReport = args.option('x-perf-report'), |
| batchMode = args.flag('batch'), |
| sourceFiles = args.rest, |
| trainSnapshot = args.flag('train-snapshot'), |
| verbose = args.flag('verbose'), |
| color = args.flag('color') { |
| // |
| // File locations. |
| // |
| defaultAnalysisOptionsPath = _absoluteNormalizedPath( |
| resourceProvider, |
| args.option(_analysisOptionsFileOption), |
| ); |
| defaultPackagesPath = _absoluteNormalizedPath( |
| resourceProvider, |
| args.option(_packagesOption), |
| ); |
| |
| // |
| // Declared variables. |
| // |
| var variables = (args[_defineVariableOption] as List).cast<String>(); |
| for (var variable in variables) { |
| var index = variable.indexOf('='); |
| if (index < 0) { |
| // TODO(brianwilkerson): Decide the semantics we want in this case. |
| // The VM prints "No value given to -D option", then tries to load '-Dfoo' |
| // as a file and dies. Unless there was nothing after the '-D', in which |
| // case it prints the warning and ignores the option. |
| } else { |
| var name = variable.substring(0, index); |
| if (name.isNotEmpty) { |
| // TODO(brianwilkerson): Decide the semantics we want in the case where |
| // there is no name. If there is no name, the VM tries to load a file |
| // named '-D' and dies. |
| declaredVariables[name] = variable.substring(index + 1); |
| } |
| } |
| } |
| } |
| |
| /// A list of the names of the experiments that are to be enabled. |
| List<String> get enabledExperiments { |
| return _argResults.multiOption(_enableExperimentOption); |
| } |
| |
| /// Update the [analysisOptions] with flags that the user specified |
| /// explicitly. The [analysisOptions] are usually loaded from one of |
| /// `analysis_options.yaml` files, possibly with includes. We consider |
| /// flags that the user specified as command line options more important, |
| /// so override the corresponding options. |
| void updateAnalysisOptions(AnalysisOptionsImpl analysisOptions) { |
| if (enabledExperiments.isNotEmpty) { |
| analysisOptions.contextFeatures = |
| FeatureSet.fromEnableFlags2( |
| sdkLanguageVersion: ExperimentStatus.currentVersion, |
| flags: enabledExperiments, |
| ) |
| as ExperimentStatus; |
| } |
| } |
| |
| /// Return a list of command-line arguments containing all of the given [args] |
| /// that are defined by the given [parser]. An argument is considered to be |
| /// defined by the parser if |
| /// - it starts with '--' and the rest of the argument (minus any value |
| /// introduced by '=') is the name of a known option, |
| /// - it starts with '-' and the rest of the argument (minus any value |
| /// introduced by '=') is the name of a known abbreviation, or |
| /// - it starts with something other than '--' or '-'. |
| /// |
| /// This function allows command-line tools to implement the |
| /// '--ignore-unrecognized-flags' option. |
| static List<String> filterUnknownArguments( |
| List<String> args, |
| ArgParser parser, |
| ) { |
| var knownOptions = <String>{}; |
| var knownAbbreviations = <String>{}; |
| parser.options.forEach((String name, Option option) { |
| knownOptions.add(name); |
| var abbreviation = option.abbr; |
| if (abbreviation != null) { |
| knownAbbreviations.add(abbreviation); |
| } |
| if (option.negatable ?? false) { |
| knownOptions.add('no-$name'); |
| } |
| }); |
| String optionName(int prefixLength, String argument) { |
| var equalsOffset = argument.lastIndexOf('='); |
| if (equalsOffset < 0) { |
| return argument.substring(prefixLength); |
| } |
| return argument.substring(prefixLength, equalsOffset); |
| } |
| |
| var filtered = <String>[]; |
| for (var i = 0; i < args.length; i++) { |
| var argument = args[i]; |
| if (argument.startsWith('--') && argument.length > 2) { |
| if (knownOptions.contains(optionName(2, argument))) { |
| filtered.add(argument); |
| } |
| } else if (argument.startsWith('-D') && argument.indexOf('=') > 0) { |
| filtered.add(argument); |
| } |
| if (argument.startsWith('-') && argument.length > 1) { |
| if (knownAbbreviations.contains(optionName(1, argument))) { |
| filtered.add(argument); |
| } |
| } else { |
| filtered.add(argument); |
| } |
| } |
| return filtered; |
| } |
| |
| /// Parse [args] into [CommandLineOptions] describing the specified |
| /// analyzer options. In case of a format error, calls [printAndFail], which |
| /// by default prints an error message to stderr and exits. |
| static CommandLineOptions? parse( |
| ResourceProvider resourceProvider, |
| List<String> args, { |
| void Function(String msg) printAndFail = printAndFail, |
| }) { |
| var options = _parse(resourceProvider, args); |
| |
| /// Only happens in testing. |
| if (options == null) { |
| return null; |
| } |
| |
| // Check SDK. |
| { |
| var sdkPath = options.dartSdkPath; |
| |
| // Check that SDK is existing directory. |
| if (sdkPath != null) { |
| if (!io.Directory(sdkPath).existsSync()) { |
| printAndFail('Invalid Dart SDK path: $sdkPath'); |
| return null; // Only reachable in testing. |
| } |
| } |
| |
| // Infer if unspecified. |
| sdkPath ??= getSdkPath(); |
| |
| var pathContext = resourceProvider.pathContext; |
| options.dartSdkPath = file_paths.absoluteNormalized(pathContext, sdkPath); |
| } |
| |
| return options; |
| } |
| |
| static String? _absoluteNormalizedPath( |
| ResourceProvider resourceProvider, |
| String? path, |
| ) { |
| if (path == null) { |
| return null; |
| } |
| var pathContext = resourceProvider.pathContext; |
| return pathContext.normalize(pathContext.absolute(path)); |
| } |
| |
| /// Add the standard flags and options to the given [parser]. The standard flags |
| /// are those that are typically used to control the way in which the code is |
| /// analyzed. |
| /// |
| // TODO(danrubel): Update DDC to support all the options defined in this method |
| // then remove the [ddc] named argument from this method. |
| static void _defineAnalysisArguments( |
| ArgParser parser, { |
| bool hide = true, |
| bool ddc = false, |
| }) { |
| parser.addOption( |
| _sdkPathOption, |
| help: 'The path to the Dart SDK.', |
| hide: ddc && hide, |
| ); |
| parser.addOption( |
| _analysisOptionsFileOption, |
| help: 'Path to an analysis options file.', |
| hide: ddc && hide, |
| ); |
| parser.addMultiOption( |
| _enableExperimentOption, |
| help: |
| 'Enable one or more experimental features. If multiple features ' |
| 'are being added, they should be comma separated.', |
| splitCommas: true, |
| ); |
| |
| // |
| // Hidden flags and options. |
| // |
| parser.addMultiOption( |
| _defineVariableOption, |
| abbr: 'D', |
| help: |
| 'Define an environment declaration. For example, "-Dfoo=bar" defines ' |
| 'an environment declaration named "foo" whose value is "bar".', |
| hide: hide, |
| ); |
| parser.addOption( |
| _packagesOption, |
| help: |
| 'The path to the package resolution configuration file, which ' |
| 'supplies a mapping of package names\ninto paths.', |
| hide: ddc, |
| ); |
| parser.addFlag( |
| _enableInitializingFormalAccessFlag, |
| help: |
| 'Enable support for allowing access to field formal parameters in a ' |
| 'constructor\'s initializer list (deprecated).', |
| defaultsTo: false, |
| negatable: false, |
| hide: hide || ddc, |
| ); |
| } |
| |
| static String _getVersion() { |
| try { |
| // This is relative to bin/snapshot, so ../.. |
| var versionPath = |
| io.Platform.script.resolve('../../version').toFilePath(); |
| var versionFile = io.File(versionPath); |
| return versionFile.readAsStringSync().trim(); |
| } catch (_) { |
| // This happens when the script is not running in the context of an SDK. |
| return '<unknown>'; |
| } |
| } |
| |
| static CommandLineOptions? _parse( |
| ResourceProvider resourceProvider, |
| List<String> args, |
| ) { |
| var verbose = args.contains('-v') || args.contains('--verbose'); |
| var hide = !verbose; |
| |
| var parser = ArgParser(allowTrailingOptions: true); |
| |
| if (!hide) { |
| parser.addSeparator('General options:'); |
| } |
| |
| // TODO(devoncarew): This defines some hidden flags, which would be better |
| // defined with the rest of the hidden flags below (to group well with the |
| // other flags). |
| _defineAnalysisArguments(parser, hide: hide); |
| |
| parser |
| ..addOption( |
| 'format', |
| help: |
| 'Specifies the format in which errors are displayed; the only ' |
| 'currently recognized values are \'json\' and \'machine\'.', |
| ) |
| ..addFlag( |
| 'version', |
| help: 'Print the analyzer version.', |
| defaultsTo: false, |
| negatable: false, |
| ) |
| ..addFlag( |
| 'help', |
| abbr: 'h', |
| help: |
| 'Display this help message. Add --verbose to show hidden options.', |
| defaultsTo: false, |
| negatable: false, |
| ) |
| ..addFlag( |
| 'verbose', |
| abbr: 'v', |
| defaultsTo: false, |
| help: 'Verbose output.', |
| negatable: false, |
| ); |
| |
| parser.addFlag( |
| 'color', |
| help: 'Use ansi colors when printing messages.', |
| defaultsTo: ansi.terminalSupportsAnsi(), |
| hide: hide, |
| ); |
| |
| // Hidden flags. |
| if (!hide) { |
| parser.addSeparator('Less frequently used flags:'); |
| } |
| |
| parser |
| ..addFlag( |
| 'batch', |
| help: 'Read commands from standard input (for testing).', |
| defaultsTo: false, |
| negatable: false, |
| hide: hide, |
| ) |
| ..addFlag( |
| _ignoreUnrecognizedFlagsFlag, |
| help: 'Ignore unrecognized command line flags.', |
| defaultsTo: false, |
| negatable: false, |
| hide: hide, |
| ) |
| ..addFlag('disable-cache-flushing', defaultsTo: false, hide: hide) |
| ..addOption( |
| 'x-perf-report', |
| help: 'Writes a performance report to the given file (experimental).', |
| hide: hide, |
| ) |
| ..addFlag( |
| 'enable-conditional-directives', |
| help: |
| 'deprecated -- Enable support for conditional directives (DEP 40).', |
| defaultsTo: false, |
| negatable: false, |
| hide: hide, |
| ) |
| ..addFlag( |
| 'log', |
| help: 'Log additional messages and exceptions.', |
| defaultsTo: false, |
| negatable: false, |
| hide: hide, |
| ) |
| ..addFlag( |
| 'use-analysis-driver-memory-byte-store', |
| help: 'Use memory byte store, not the file system cache.', |
| defaultsTo: false, |
| negatable: false, |
| hide: hide, |
| ) |
| ..addMultiOption( |
| 'url-mapping', |
| help: |
| '--url-mapping=libraryUri,/path/to/library.dart directs the ' |
| 'analyzer to use "library.dart" as the source for an import ' |
| 'of "libraryUri".', |
| splitCommas: false, |
| hide: hide, |
| ) |
| ..addFlag( |
| 'train-snapshot', |
| help: |
| 'Analyze the given source for the purposes of training a ' |
| 'dartanalyzer snapshot.', |
| hide: hide, |
| negatable: false, |
| ); |
| |
| try { |
| if (args.contains('--$_ignoreUnrecognizedFlagsFlag')) { |
| args = filterUnknownArguments(args, parser); |
| } |
| var results = parser.parse(args); |
| |
| // Help requests. |
| if (results.flag('help')) { |
| _showUsage(parser, fromHelp: true); |
| exitHandler(0); |
| return null; // Only reachable in testing. |
| } |
| |
| // Batch mode and input files. |
| if (results.flag('batch')) { |
| if (results.rest.isNotEmpty) { |
| errorSink.writeln('No source files expected in the batch mode.'); |
| _showUsage(parser); |
| exitHandler(15); |
| return null; // Only reachable in testing. |
| } |
| } else if (results.flag('version')) { |
| outSink.writeln('$_binaryName version ${_getVersion()}'); |
| exitHandler(0); |
| return null; // Only reachable in testing. |
| } else { |
| if (results.rest.isEmpty) { |
| _showUsage(parser, fromHelp: true); |
| exitHandler(15); |
| return null; // Only reachable in testing. |
| } |
| } |
| |
| if (results.wasParsed(_enableExperimentOption)) { |
| var names = results.multiOption(_enableExperimentOption); |
| var errorFound = false; |
| for (var validationResult in validateFlags(names)) { |
| if (validationResult.isError) { |
| errorFound = true; |
| } |
| var kind = validationResult.isError ? 'ERROR' : 'WARNING'; |
| errorSink.writeln('$kind: ${validationResult.message}'); |
| } |
| if (errorFound) { |
| _showUsage(parser); |
| exitHandler(15); |
| return null; // Only reachable in testing. |
| } |
| } |
| |
| return CommandLineOptions._fromArgs(resourceProvider, results); |
| } on FormatException catch (e) { |
| errorSink.writeln(e.message); |
| _showUsage(parser); |
| exitHandler(15); |
| return null; // Only reachable in testing. |
| } |
| } |
| |
| static void _showUsage(ArgParser parser, {bool fromHelp = false}) { |
| errorSink.writeln( |
| 'Usage: $_binaryName [options...] <directory or list of files>', |
| ); |
| |
| errorSink.writeln(''); |
| errorSink.writeln(parser.usage); |
| |
| errorSink.writeln(''); |
| errorSink.writeln(''' |
| Run "dartanalyzer -h -v" for verbose help output, including less commonly used options. |
| For more information, see https://dart.dev/tools/dartanalyzer.\n'''); |
| } |
| } |