| // 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/analysis_options/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]. | |
| new _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); | |
| } | |
| /// Configure the [analysisOptionsBuilder] with flags that the user specified | |
| /// explicitly. The builder is usually initialized 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 configureAnalysisOptionsBuilder( | |
| AnalysisOptionsBuilder analysisOptionsBuilder, | |
| ) { | |
| if (enabledExperiments.isNotEmpty) { | |
| analysisOptionsBuilder.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 pathContext = resourceProvider.pathContext; | |
| var sdkPath = options.dartSdkPath; | |
| if (sdkPath != null) { | |
| var normalized = file_paths.absoluteNormalized(pathContext, sdkPath); | |
| if (!resourceProvider.getFolder(normalized).exists) { | |
| printAndFail('Invalid Dart SDK path: $sdkPath'); | |
| return null; // Only reachable in testing. | |
| } | |
| } | |
| // Infer if unspecified. | |
| sdkPath ??= getSdkPath(); | |
| 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'''); | |
| } | |
| } |