blob: 5e53bcd6fa09e51efb21b232833fbd2e56ab354f [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/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''');
}
}