blob: c761f7680bfe7c4ce9771b4fb084ca9be27293fd [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.
library analyzer.src.task.options;
import 'dart:collection';
import 'package:analyzer/analyzer.dart';
import 'package:analyzer/plugin/options.dart';
import 'package:analyzer/source/analysis_options_provider.dart';
import 'package:analyzer/source/error_processor.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/java_engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_general.dart';
import 'package:analyzer/src/lint/config.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/lint/options_rule_validator.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer/src/task/general.dart';
import 'package:analyzer/src/util/yaml.dart';
import 'package:analyzer/task/general.dart';
import 'package:analyzer/task/model.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
/// The errors produced while parsing an analysis options file.
///
/// The list will be empty if there were no errors, but will not be `null`.
final ListResultDescriptor<AnalysisError> ANALYSIS_OPTIONS_ERRORS =
new ListResultDescriptor<AnalysisError>(
'ANALYSIS_OPTIONS_ERRORS', AnalysisError.NO_ERRORS);
final _OptionsProcessor _processor = new _OptionsProcessor();
void applyToAnalysisOptions(
AnalysisOptionsImpl options, Map<String, Object> optionMap) {
_processor.applyToAnalysisOptions(options, optionMap);
}
/// `analyzer` analysis options constants.
class AnalyzerOptions {
static const String analyzer = 'analyzer';
static const String enableAssertInitializer = 'enableAssertInitializer';
static const String enableAsync = 'enableAsync';
static const String enableGenericMethods = 'enableGenericMethods';
static const String enableInitializingFormalAccess =
'enableInitializingFormalAccess';
static const String enableStrictCallChecks = 'enableStrictCallChecks';
static const String enableSuperMixins = 'enableSuperMixins';
static const String errors = 'errors';
static const String exclude = 'exclude';
static const String include = 'include';
static const String language = 'language';
static const String plugins = 'plugins';
static const String strong_mode = 'strong-mode';
// Strong mode options, see AnalysisOptionsImpl for documentation.
static const String implicitCasts = 'implicit-casts';
static const String implicitDynamic = 'implicit-dynamic';
/// Ways to say `ignore`.
static const List<String> ignoreSynonyms = const ['ignore', 'false'];
/// Valid error `severity`s.
static final List<String> severities =
new List.unmodifiable(severityMap.keys);
/// Ways to say `include`.
static const List<String> includeSynonyms = const ['include', 'true'];
/// Ways to say `true` or `false`.
static const List<String> trueOrFalse = const ['true', 'false'];
/// Supported top-level `analyzer` options.
static const List<String> topLevel = const [
errors,
exclude,
language,
plugins,
strong_mode
];
/// Supported `analyzer` language configuration options.
static const List<String> languageOptions = const [
enableAssertInitializer,
enableAsync,
enableGenericMethods,
enableStrictCallChecks,
enableSuperMixins
];
}
/// Validates `analyzer` options.
class AnalyzerOptionsValidator extends CompositeValidator {
AnalyzerOptionsValidator()
: super([
new TopLevelAnalyzerOptionsValidator(),
new StrongModeOptionValueValidator(),
new ErrorFilterOptionValidator(),
new LanguageOptionValidator()
]);
}
/// Convenience class for composing validators.
class CompositeValidator extends OptionsValidator {
final List<OptionsValidator> validators;
CompositeValidator(this.validators);
@override
void validate(ErrorReporter reporter, Map<String, YamlNode> options) =>
validators.forEach((v) => v.validate(reporter, options));
}
/// Builds error reports with value proposals.
class ErrorBuilder {
String proposal;
AnalysisOptionsWarningCode code;
/// Create a builder for the given [supportedOptions].
ErrorBuilder(List<String> supportedOptions) {
assert(supportedOptions != null && !supportedOptions.isEmpty);
if (supportedOptions.length > 1) {
proposal = StringUtilities.printListOfQuotedNames(supportedOptions);
code = pluralProposalCode;
} else {
proposal = "'${supportedOptions.join()}'";
code = singularProposalCode;
}
}
AnalysisOptionsWarningCode get pluralProposalCode =>
AnalysisOptionsWarningCode.UNSUPPORTED_OPTION_WITH_LEGAL_VALUES;
AnalysisOptionsWarningCode get singularProposalCode =>
AnalysisOptionsWarningCode.UNSUPPORTED_OPTION_WITH_LEGAL_VALUE;
/// Report an unsupported [node] value, defined in the given [scopeName].
void reportError(ErrorReporter reporter, String scopeName, YamlNode node) {
reporter
.reportErrorForSpan(code, node.span, [scopeName, node.value, proposal]);
}
}
/// Validates `analyzer` error filter options.
class ErrorFilterOptionValidator extends OptionsValidator {
/// Legal values.
static final List<String> legalValues =
new List.from(AnalyzerOptions.ignoreSynonyms)
..addAll(AnalyzerOptions.includeSynonyms)
..addAll(AnalyzerOptions.severities);
/// Pretty String listing legal values.
static final String legalValueString =
StringUtilities.printListOfQuotedNames(legalValues);
/// Lazily populated set of error codes (hashed for speedy lookup).
static HashSet<String> _errorCodes;
/// Legal error code names.
static Set<String> get errorCodes {
if (_errorCodes == null) {
_errorCodes = new HashSet<String>();
// Engine codes.
_errorCodes.addAll(errorCodeValues.map((ErrorCode code) => code.name));
}
return _errorCodes;
}
@override
void validate(ErrorReporter reporter, Map<String, YamlNode> options) {
var analyzer = options[AnalyzerOptions.analyzer];
if (analyzer is YamlMap) {
var filters = analyzer[AnalyzerOptions.errors];
if (filters is YamlMap) {
String value;
filters.nodes.forEach((k, v) {
if (k is YamlScalar) {
value = toUpperCase(k.value);
if (!errorCodes.contains(value)) {
reporter.reportErrorForSpan(
AnalysisOptionsWarningCode.UNRECOGNIZED_ERROR_CODE,
k.span,
[k.value?.toString()]);
}
}
if (v is YamlScalar) {
value = toLowerCase(v.value);
if (!legalValues.contains(value)) {
reporter.reportErrorForSpan(
AnalysisOptionsWarningCode
.UNSUPPORTED_OPTION_WITH_LEGAL_VALUES,
v.span,
[
AnalyzerOptions.errors,
v.value?.toString(),
legalValueString
]);
}
}
});
}
}
}
}
/// A task that generates errors for an analysis options file.
class GenerateOptionsErrorsTask extends SourceBasedAnalysisTask {
/// The name of the input whose value is the content of the file.
static const String CONTENT_INPUT_NAME = 'CONTENT_INPUT_NAME';
/// The task descriptor describing this kind of task.
static final TaskDescriptor DESCRIPTOR = new TaskDescriptor(
'GenerateOptionsErrorsTask',
createTask,
buildInputs,
<ResultDescriptor>[ANALYSIS_OPTIONS_ERRORS, LINE_INFO],
suitabilityFor: suitabilityFor);
AnalysisOptionsProvider optionsProvider;
GenerateOptionsErrorsTask(AnalysisContext context, AnalysisTarget target)
: super(context, target) {
optionsProvider = new AnalysisOptionsProvider(context?.sourceFactory);
}
@override
TaskDescriptor get descriptor => DESCRIPTOR;
Source get source => target.source;
@override
void internalPerform() {
String content = getRequiredInput(CONTENT_INPUT_NAME);
//
// Record outputs.
//
outputs[ANALYSIS_OPTIONS_ERRORS] =
analyzeAnalysisOptions(source, content, context?.sourceFactory);
outputs[LINE_INFO] = computeLineInfo(content);
}
static List<AnalysisError> analyzeAnalysisOptions(
Source source, String content, SourceFactory sourceFactory) {
List<AnalysisError> errors = <AnalysisError>[];
Source initialSource = source;
SourceSpan initialIncludeSpan;
AnalysisOptionsProvider optionsProvider =
new AnalysisOptionsProvider(sourceFactory);
// Validate the specified options and any included option files
void validate(Source source, Map<String, YamlNode> options) {
List<AnalysisError> validationErrors =
new OptionsFileValidator(source).validate(options);
if (initialIncludeSpan != null && validationErrors.isNotEmpty) {
for (AnalysisError error in validationErrors) {
var args = [
source.fullName,
error.offset.toString(),
(error.offset + error.length - 1).toString(),
error.message,
];
errors.add(new AnalysisError(
initialSource,
initialIncludeSpan.start.column + 1,
initialIncludeSpan.length,
AnalysisOptionsWarningCode.INCLUDED_FILE_WARNING,
args));
}
} else {
errors.addAll(validationErrors);
}
YamlNode node = options[AnalyzerOptions.include];
if (node == null) {
return;
}
SourceSpan span = node.span;
initialIncludeSpan ??= span;
String includeUri = span.text;
Source includedSource = sourceFactory.resolveUri(source, includeUri);
if (!includedSource.exists()) {
errors.add(new AnalysisError(
initialSource,
initialIncludeSpan.start.column + 1,
initialIncludeSpan.length,
AnalysisOptionsWarningCode.INCLUDE_FILE_NOT_FOUND,
[includeUri, source.fullName]));
return;
}
try {
Map<String, YamlNode> options =
optionsProvider.getOptionsFromString(includedSource.contents.data);
validate(includedSource, options);
} on OptionsFormatException catch (e) {
var args = [
includedSource.fullName,
e.span.start.offset.toString(),
e.span.end.offset.toString(),
e.message,
];
// Report errors for included option files
// on the include directive located in the initial options file.
errors.add(new AnalysisError(
initialSource,
initialIncludeSpan.start.column + 1,
initialIncludeSpan.length,
AnalysisOptionsErrorCode.INCLUDED_FILE_PARSE_ERROR,
args));
}
}
try {
Map<String, YamlNode> options =
optionsProvider.getOptionsFromString(content);
validate(source, options);
} on OptionsFormatException catch (e) {
SourceSpan span = e.span;
errors.add(new AnalysisError(source, span.start.column + 1, span.length,
AnalysisOptionsErrorCode.PARSE_ERROR, [e.message]));
}
return errors;
}
/// Return a map from the names of the inputs of this kind of task to the
/// task input descriptors describing those inputs for a task with the
/// given [target].
static Map<String, TaskInput> buildInputs(AnalysisTarget source) =>
<String, TaskInput>{CONTENT_INPUT_NAME: CONTENT.of(source)};
/// Compute [LineInfo] for the given [content].
static LineInfo computeLineInfo(String content) {
List<int> lineStarts = StringUtilities.computeLineStarts(content);
return new LineInfo(lineStarts);
}
/// Create a task based on the given [target] in the given [context].
static GenerateOptionsErrorsTask createTask(
AnalysisContext context, AnalysisTarget target) =>
new GenerateOptionsErrorsTask(context, target);
/**
* Return an indication of how suitable this task is for the given [target].
*/
static TaskSuitability suitabilityFor(AnalysisTarget target) {
if (target is Source &&
(target.shortName == AnalysisEngine.ANALYSIS_OPTIONS_FILE ||
target.shortName == AnalysisEngine.ANALYSIS_OPTIONS_YAML_FILE)) {
return TaskSuitability.HIGHEST;
}
return TaskSuitability.NONE;
}
}
/// Validates `analyzer` language configuration options.
class LanguageOptionValidator extends OptionsValidator {
ErrorBuilder builder = new ErrorBuilder(AnalyzerOptions.languageOptions);
ErrorBuilder trueOrFalseBuilder = new TrueOrFalseValueErrorBuilder();
@override
void validate(ErrorReporter reporter, Map<String, YamlNode> options) {
var analyzer = options[AnalyzerOptions.analyzer];
if (analyzer is YamlMap) {
var language = analyzer[AnalyzerOptions.language];
if (language is YamlMap) {
language.nodes.forEach((k, v) {
String key, value;
bool validKey = false;
if (k is YamlScalar) {
key = k.value?.toString();
if (!AnalyzerOptions.languageOptions.contains(key)) {
builder.reportError(reporter, AnalyzerOptions.language, k);
} else {
// If we have a valid key, go on and check the value.
validKey = true;
}
}
if (validKey && v is YamlScalar) {
value = toLowerCase(v.value);
if (!AnalyzerOptions.trueOrFalse.contains(value)) {
trueOrFalseBuilder.reportError(reporter, key, v);
}
}
});
}
}
}
}
/// Validates `linter` top-level options.
/// TODO(pq): move into `linter` package and plugin.
class LinterOptionsValidator extends TopLevelOptionValidator {
LinterOptionsValidator() : super('linter', const ['rules']);
}
/// Validates options defined in an analysis options file.
class OptionsFileValidator {
/// The source being validated.
final Source source;
final List<OptionsValidator> _validators = [
new AnalyzerOptionsValidator(),
new LinterOptionsValidator(),
new LinterRuleOptionsValidator()
];
OptionsFileValidator(this.source);
List<AnalysisError> validate(Map<String, YamlNode> options) {
RecordingErrorListener recorder = new RecordingErrorListener();
ErrorReporter reporter = new ErrorReporter(recorder, source);
_validators.forEach((OptionsValidator v) => v.validate(reporter, options));
return recorder.errors;
}
}
/// Validates `analyzer` strong-mode value configuration options.
class StrongModeOptionValueValidator extends OptionsValidator {
ErrorBuilder trueOrFalseBuilder = new TrueOrFalseValueErrorBuilder();
@override
void validate(ErrorReporter reporter, Map<String, YamlNode> options) {
var analyzer = options[AnalyzerOptions.analyzer];
if (analyzer is YamlMap) {
var v = analyzer.nodes[AnalyzerOptions.strong_mode];
if (v is YamlScalar) {
var value = toLowerCase(v.value);
if (!AnalyzerOptions.trueOrFalse.contains(value)) {
trueOrFalseBuilder.reportError(
reporter, AnalyzerOptions.strong_mode, v);
}
}
}
}
}
/// Validates `analyzer` top-level options.
class TopLevelAnalyzerOptionsValidator extends TopLevelOptionValidator {
TopLevelAnalyzerOptionsValidator()
: super(AnalyzerOptions.analyzer, AnalyzerOptions.topLevel);
}
/// Validates top-level options. For example,
/// plugin:
/// top-level-option: true
class TopLevelOptionValidator extends OptionsValidator {
final String pluginName;
final List<String> supportedOptions;
String _valueProposal;
AnalysisOptionsWarningCode _warningCode;
TopLevelOptionValidator(this.pluginName, this.supportedOptions) {
assert(supportedOptions != null && !supportedOptions.isEmpty);
if (supportedOptions.length > 1) {
_valueProposal = StringUtilities.printListOfQuotedNames(supportedOptions);
_warningCode =
AnalysisOptionsWarningCode.UNSUPPORTED_OPTION_WITH_LEGAL_VALUES;
} else {
_valueProposal = "'${supportedOptions.join()}'";
_warningCode =
AnalysisOptionsWarningCode.UNSUPPORTED_OPTION_WITH_LEGAL_VALUE;
}
}
@override
void validate(ErrorReporter reporter, Map<String, YamlNode> options) {
YamlNode node = options[pluginName];
if (node is YamlMap) {
node.nodes.forEach((k, v) {
if (k is YamlScalar) {
if (!supportedOptions.contains(k.value)) {
reporter.reportErrorForSpan(
_warningCode, k.span, [pluginName, k.value, _valueProposal]);
}
}
//TODO(pq): consider an error if the node is not a Scalar.
});
}
}
}
/// An error-builder that knows about `true` and `false` legal values.
class TrueOrFalseValueErrorBuilder extends ErrorBuilder {
TrueOrFalseValueErrorBuilder() : super(AnalyzerOptions.trueOrFalse);
@override
AnalysisOptionsWarningCode get pluralProposalCode =>
AnalysisOptionsWarningCode.UNSUPPORTED_VALUE;
}
class _OptionsProcessor {
static final Map<String, Object> defaults = {'analyzer': {}};
/**
* Apply the options in the given [optionMap] to the given analysis [options].
*/
void applyToAnalysisOptions(
AnalysisOptionsImpl options, Map<String, Object> optionMap) {
if (optionMap == null) {
return;
}
var analyzer = optionMap[AnalyzerOptions.analyzer];
if (analyzer is Map) {
// Process strong mode option.
var strongMode = analyzer[AnalyzerOptions.strong_mode];
_applyStrongOptions(options, strongMode);
// Set filters.
var filters = analyzer[AnalyzerOptions.errors];
_applyProcessors(options, filters);
// Process language options.
var language = analyzer[AnalyzerOptions.language];
_applyLanguageOptions(options, language);
// Process excludes.
var excludes = analyzer[AnalyzerOptions.exclude];
_applyExcludes(options, excludes);
}
LintConfig config = parseConfig(optionMap);
if (config != null) {
Iterable<LintRule> lintRules = Registry.ruleRegistry.enabled(config);
if (lintRules.isNotEmpty) {
options.lint = true;
options.lintRules = lintRules.toList();
}
}
}
void _applyExcludes(AnalysisOptionsImpl options, Object excludes) {
if (excludes is YamlList) {
List<String> excludeList = toStringList(excludes);
if (excludeList != null) {
options.excludePatterns = excludeList;
}
}
}
void _applyLanguageOption(
AnalysisOptionsImpl options, Object feature, Object value) {
bool boolValue = toBool(value);
if (boolValue != null) {
if (feature == AnalyzerOptions.enableAssertInitializer) {
options.enableAssertInitializer = boolValue;
} else if (feature == AnalyzerOptions.enableStrictCallChecks) {
options.enableStrictCallChecks = boolValue;
} else if (feature == AnalyzerOptions.enableSuperMixins) {
options.enableSuperMixins = boolValue;
}
}
}
void _applyLanguageOptions(AnalysisOptionsImpl options, Object configs) {
if (configs is YamlMap) {
configs.nodes.forEach((key, value) {
if (key is YamlScalar && value is YamlScalar) {
String feature = key.value?.toString();
_applyLanguageOption(options, feature, value.value);
}
});
} else if (configs is Map) {
configs
.forEach((key, value) => _applyLanguageOption(options, key, value));
}
}
void _applyProcessors(AnalysisOptionsImpl options, Object codes) {
ErrorConfig config = new ErrorConfig(codes);
options.errorProcessors = config.processors;
}
void _applyStrongModeOption(
AnalysisOptionsImpl options, Object feature, Object value) {
bool boolValue = toBool(value);
if (boolValue != null) {
if (feature == AnalyzerOptions.implicitCasts) {
options.implicitCasts = boolValue;
}
if (feature == AnalyzerOptions.implicitDynamic) {
options.implicitDynamic = boolValue;
}
}
}
void _applyStrongOptions(AnalysisOptionsImpl options, Object config) {
if (config is YamlMap) {
options.strongMode = true;
config.nodes.forEach((k, v) {
if (k is YamlScalar && v is YamlScalar) {
_applyStrongModeOption(options, k.value?.toString(), v.value);
}
});
} else if (config is Map) {
options.strongMode = true;
config.forEach((k, v) => _applyStrongModeOption(options, k, v));
} else {
options.strongMode = config is bool ? config : false;
}
}
}