blob: 13844a14f5408eb01f70c8470339a20a14da4ae9 [file] [log] [blame]
// Copyright (c) 2016, 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 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
import 'package:analyzer/src/analysis_options/error/option_codes.dart';
import 'package:analyzer/src/diagnostic/diagnostic_factory.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer/src/lint/state.dart';
import 'package:analyzer/src/plugin/options.dart';
import 'package:analyzer/src/util/yaml.dart';
import 'package:analyzer/src/utilities/uri_cache.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:yaml/yaml.dart';
/// Validates `linter` rule configurations.
class LinterRuleOptionsValidator extends OptionsValidator {
static const includeKey = 'include';
static const linter = 'linter';
static const rulesKey = 'rules';
static final diagnosticFactory = DiagnosticFactory();
final VersionConstraint? sdkVersionConstraint;
final bool sourceIsOptionsForContextRoot;
final AnalysisOptionsProvider optionsProvider;
final ResourceProvider resourceProvider;
LinterRuleOptionsValidator({
required this.resourceProvider,
required this.optionsProvider,
this.sdkVersionConstraint,
this.sourceIsOptionsForContextRoot = true,
});
bool currentSdkAllows(Version? since) {
if (since == null) return true;
var sdk = sdkVersionConstraint;
if (sdk == null) return false;
return sdk.allows(since);
}
AbstractAnalysisRule? getRegisteredLint(Object value) => Registry
.ruleRegistry
.rules
.firstWhereOrNull((rule) => rule.name == value);
bool isDeprecatedInCurrentSdk(DeprecatedState state) =>
currentSdkAllows(state.since);
bool isRemovedInCurrentSdk(State state) {
if (state is! RemovedState) return false;
return currentSdkAllows(state.since);
}
@override
List<Diagnostic> validate(ErrorReporter reporter, YamlMap options) {
var includeRules = <YamlScalar, Set<String>>{};
var node = options.valueAt(linter);
var includeNode = options.valueAt(includeKey);
if (includeNode != null) {
includeRules = _processIncludes(includeNode, reporter);
}
if (node is YamlMap) {
var rules = node.valueAt(rulesKey);
_validateRules(rules, reporter, includeRules);
}
return const [];
}
Uri? _actualIncludePath(String includePath, Uri? sourceUri) {
var (first, last) = (
includePath.codeUnits.firstOrNull,
includePath.codeUnits.lastOrNull,
);
if ((first == 0x0022 || first == 0x0027) && first == last) {
// The URI begins and ends with either a double quote or single quote
// i.e. the value of the "include" field is quoted.
includePath = includePath.substring(1, includePath.length - 1);
}
if (includePath.isEmpty) return null;
var uri = Uri.parse(includePath);
if (uri == sourceUri) {
// The URI is the same as the source URI, so we don't need to resolve it.
return null;
}
if (uri.isAbsolute) {
// The URI is absolute, so we don't need to resolve it.
return uri;
}
if (sourceUri == null) {
// The URI is relative, but we don't have a base URI to resolve it
// against.
return null;
}
return uriCache.resolveRelative(sourceUri, uri);
}
Set<String> _collectRules(YamlNode? rules) {
var includeRules = <String>{};
if (rules is YamlList) {
for (var ruleNode in rules.nodes) {
var value = ruleNode.value;
if (value != null) {
var rule = getRegisteredLint(value as Object);
if (rule != null) {
includeRules.add(rule.name);
}
}
}
} else if (rules is YamlMap) {
for (var entry in rules.nodeMap.entries) {
var value = entry.key.value as Object?;
if (value == null) {
continue;
}
var enabled = entry.value.value;
if (enabled is! bool) {
continue;
}
if (enabled) {
var rule = getRegisteredLint(value);
if (rule != null) {
includeRules.add(rule.name);
}
}
}
}
return includeRules;
}
/// Returns the first rule that is incompatible with the given [rule].
///
/// If the rule is found in the [activeRules] set, it returns the rule name
/// and null for the file path.
///
/// If the rule is found in the [includeRules] map, it returns the file path
/// and the rule name.
(YamlScalar?, String)? _findIncompatibleRule(
AbstractAnalysisRule rule, {
required Set<String> activeRules,
required Map<YamlScalar, Set<String>> includeRules,
}) {
for (var incompatibleRule in rule.incompatibleRules) {
if (activeRules.contains(incompatibleRule)) {
return (null, incompatibleRule);
}
for (var MapEntry(key: filePath, value: rules) in includeRules.entries) {
if (rules.contains(incompatibleRule)) {
return (filePath, incompatibleRule);
}
}
}
return null;
}
Map<YamlScalar, Set<String>> _processIncludes(
YamlNode includeNode,
ErrorReporter reporter,
) {
var seenRules = <YamlScalar, Set<String>>{};
var includes = <(YamlScalar, String)>[];
if (includeNode is YamlScalar) {
includes.add((includeNode, includeNode.value.toString()));
} else if (includeNode is YamlList) {
for (var node in includeNode.nodes) {
if (node is YamlScalar) {
includes.add((node, node.value.toString()));
}
}
}
var uri = includeNode.span.sourceUrl;
for (var (includeNode, includePath) in includes) {
File file;
try {
var pathStr = _actualIncludePath(includePath, uri);
if (pathStr == null) continue;
file = resourceProvider.getFile(fromUri(pathStr));
} catch (_) {
// if files are invalid, we ignore them
continue;
}
var includedOptions = optionsProvider.getOptionsFromFile(file);
var linterNode = includedOptions.valueAt(linter);
if (linterNode is YamlMap) {
var rulesNode = linterNode.valueAt(rulesKey);
var rules = _collectRules(rulesNode);
AbstractAnalysisRule? lintRule;
YamlScalar? filePath;
String? incompatible;
for (var rule in rules) {
lintRule = getRegisteredLint(rule);
if (lintRule == null) {
continue;
}
var record = _findIncompatibleRule(
lintRule,
activeRules: {},
includeRules: seenRules,
);
if (record != null) {
filePath = record.$1!;
incompatible = record.$2;
break;
}
}
if (filePath != null && incompatible != null && lintRule != null) {
// Report the first incompatible rule found.
reporter.reportError(
diagnosticFactory.incompatibleIncludedLint(
source: FileSource(
resourceProvider.getFile(
includeNode.span.sourceUrl!.toFilePath(),
),
),
referenceRule: lintRule.name,
incompatibleRule: incompatible,
reference: includeNode,
incompatible: filePath,
),
);
}
seenRules[includeNode] = rules;
}
}
return seenRules;
}
void _validateRules(
YamlNode? rules,
ErrorReporter reporter,
Map<YamlScalar, Set<String>> includeRules,
) {
var activeRules = <String>{};
void validateRule(YamlNode node, Object? enabled) {
var value = node.value;
if (value == null) return;
if (enabled == null) return;
var rule = getRegisteredLint(value as Object);
if (rule == null) {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.UNDEFINED_LINT,
arguments: [value],
);
return;
}
Object? enabledValue;
if (enabled is YamlNode) {
enabledValue = enabled.value;
} else if (enabled is bool) {
enabledValue = enabled;
}
if (enabledValue == null) {
return;
}
if (enabledValue is! bool) {
var warningNode = enabled is YamlNode ? enabled : node;
reporter.atSourceSpan(
warningNode.span,
AnalysisOptionsWarningCode.UNSUPPORTED_VALUE,
arguments: [value, enabledValue, "'true' or 'false'"],
);
return;
}
if (enabledValue) {
var incompatible = _findIncompatibleRule(
rule,
activeRules: activeRules,
includeRules: includeRules,
);
if (incompatible != null) {
var (filePath, incompatibleRule) = incompatible;
if (filePath == null) {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.INCOMPATIBLE_LINT,
arguments: [value, incompatibleRule],
);
} else {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.INCOMPATIBLE_LINT_FILE,
arguments: [value, incompatibleRule, filePath.toString()],
);
}
} else if (!activeRules.add(rule.name)) {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.DUPLICATE_RULE,
arguments: [value],
);
}
}
// Report removed or deprecated lint warnings defined directly (and not in
// includes).
if (sourceIsOptionsForContextRoot) {
var state = rule.state;
if (state is DeprecatedState && isDeprecatedInCurrentSdk(state)) {
var replacedBy = state.replacedBy;
if (replacedBy != null) {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.DEPRECATED_LINT_WITH_REPLACEMENT,
arguments: [value, replacedBy],
);
} else {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.DEPRECATED_LINT,
arguments: [value],
);
}
} else if (isRemovedInCurrentSdk(state)) {
var since = state.since.toString();
var replacedBy = (state as RemovedState).replacedBy;
if (replacedBy != null) {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.REPLACED_LINT,
arguments: [value, since, replacedBy],
);
} else {
reporter.atSourceSpan(
node.span,
AnalysisOptionsWarningCode.REMOVED_LINT,
arguments: [value, since],
);
}
}
}
}
if (rules is YamlList) {
for (var ruleNode in rules.nodes) {
validateRule(ruleNode, true);
}
} else if (rules is YamlMap) {
for (var MapEntry(:key, :value) in rules.nodeMap.entries) {
validateRule(key, value);
}
}
}
}