blob: cdabea1b7c54ee9ff83f0670e25d1e577723a552 [file]
// Copyright (c) 2026, 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.
part of 'analysis_options_parser.dart';
void _reportUnsupportedSectionKeys({
required YamlMap section,
required String sectionName,
required Set<String> supportedOptions,
required DiagnosticReporter reporter,
}) {
var valueProposal = supportedOptions.quotedAndCommaSeparatedWithAnd;
for (var keyNode in section.nodes.keys) {
if (keyNode is YamlScalar && !supportedOptions.contains(keyNode.value)) {
var optionKey = keyNode.value.toString();
var locatableDiagnostic = supportedOptions.length == 1
? diag.unsupportedOptionWithLegalValue.withArguments(
sectionName: sectionName,
optionKey: optionKey,
legalValue: valueProposal,
)
: diag.unsupportedOptionWithLegalValues.withArguments(
sectionName: sectionName,
optionKey: optionKey,
legalValues: valueProposal,
);
reporter.report(locatableDiagnostic.atSourceSpan(keyNode.span));
}
}
}
/// The content of a file.
final class AnalysisOptionsFileContent {
/// The file text.
final String text;
/// Line information for [text].
final LineInfo lineInfo;
/// The YAML parsed from [text].
///
/// This is `null` if [text] cannot be parsed as YAML.
final YamlNode? yaml;
AnalysisOptionsFileContent({
required this.text,
required this.lineInfo,
required this.yaml,
});
/// The YAML parsed from [text], if it is a map.
YamlMap? get yamlMap => yaml.tryCast<YamlMap>();
}
abstract final class _DiagnosticOptions {
static final Set<String> codeNames = diagnosticCodeValues
.map((DiagnosticCode code) => code.lowerCaseName.toUpperCase())
.toSet();
static final List<String> errorProcessorLegalValues = [
...AnalysisOptionsFileKeys.ignoreSynonyms,
...AnalysisOptionsFileKeys.trueOrFalse,
...AnalysisOptionsFileKeys.severities,
];
/// The diagnostic code names that existed, but were removed.
///
/// We don't want to report these, this breaks clients.
// TODO(scheglov): https://github.com/flutter/flutter/issues/141576
static const Set<String> removedCodeNames = {'MISSING_RETURN'};
static Set<String> currentLintCodeNames() {
// TODO(scheglov): Move lint-code lookup behind Registry so callers don't
// need to rebuild this mutable-registry snapshot.
return Registry.ruleRegistry.rules
.map((rule) => rule.name.toUpperCase())
.toSet();
}
static bool isKnownDiagnosticOrLintCode(
String upperCaseName,
Set<String> lintCodeNames,
) {
// TODO(scheglov): Review whether options parsing should keep
// accepting removed diagnostic codes as a compatibility case.
return codeNames.contains(upperCaseName) ||
lintCodeNames.contains(upperCaseName) ||
removedCodeNames.contains(upperCaseName);
}
}
/// Builds error reports with value proposals.
class _ErrorBuilder {
/// Report an unsupported `node` value, defined in the given `scopeName`.
void Function(DiagnosticReporter reporter, String scopeName, YamlNode node)
reportError;
/// Create a builder for the given [supportedOptions].
factory _ErrorBuilder(Set<String> supportedOptions) {
if (supportedOptions.isEmpty) {
return _ErrorBuilder._(
reportError: (reporter, scopeName, node) => reporter.report(
diag.unsupportedOptionWithoutValues
.withArguments(
sectionName: scopeName,
optionKey: node.value.toString(),
)
.atSourceSpan(node.span),
),
);
} else if (supportedOptions.length == 1) {
return _ErrorBuilder._(
reportError: (reporter, scopeName, node) => reporter.report(
diag.unsupportedOptionWithLegalValue
.withArguments(
sectionName: scopeName,
optionKey: node.value.toString(),
legalValue: supportedOptions.single,
)
.atSourceSpan(node.span),
),
);
} else {
return _ErrorBuilder._(
reportError: (reporter, scopeName, node) => reporter.report(
diag.unsupportedOptionWithLegalValues
.withArguments(
sectionName: scopeName,
optionKey: node.value.toString(),
legalValues: supportedOptions.quotedAndCommaSeparatedWithAnd,
)
.atSourceSpan(node.span),
),
);
}
}
_ErrorBuilder._({required this.reportError});
}
/// Content read and parsed from one file.
sealed class _FileContent {
final File file;
_FileContent({required this.file});
AnalysisOptionsFileContent? get content;
}
/// A file node in the include graph for one parse request.
///
/// The node is [SourceFactory] specific because it includes resolved include
/// edges.
sealed class _FileNode {
final File file;
_FileNode({required this.file});
AnalysisOptionsFileContent? get content;
}
/// A YAML parse failure for a file.
final class _FileParseFailure {
final String message;
final SourceSpan? span;
_FileParseFailure({required this.message, required this.span});
}
/// One valid `include` directive in a parsed file.
final class _IncludeDirective {
final YamlScalar node;
final String uri;
_IncludeDirective({required this.node, required this.uri});
}
/// The resolved meaning of one `include` directive.
sealed class _IncludeResolution {
final _IncludeDirective include;
_IncludeResolution({required this.include});
}
/// A parsed string legacy plugin occurrence.
///
/// Non-string entries in `analyzer/plugins` are ignored for multiple-plugin
/// diagnostics, so this model only represents names that participate in legacy
/// plugin precedence checks.
final class _LegacyPluginData {
final YamlNode node;
final String name;
_LegacyPluginData({required this.node, required this.name});
}
/// An `include` directive whose target was read but had malformed YAML.
final class _MalformedInclude extends _IncludeResolution {
final _MalformedYamlFileNode file;
_MalformedInclude({required super.include, required this.file});
}
final class _MalformedYamlFileContent extends _FileContent {
@override
final AnalysisOptionsFileContent content;
final _FileParseFailure failure;
_MalformedYamlFileContent({
required super.file,
required this.content,
required this.failure,
});
}
/// A readable file whose YAML root could not be parsed.
final class _MalformedYamlFileNode extends _FileNode {
@override
final AnalysisOptionsFileContent content;
final _FileParseFailure failure;
_MalformedYamlFileNode({
required super.file,
required this.content,
required this.failure,
});
}
/// An `include` directive whose target could not be resolved to a parsed file.
final class _MissingInclude extends _IncludeResolution {
_MissingInclude({required super.include});
}
/// Local `analyzer` section semantics computed from one parsed options file.
final class _ParsedAnalyzerData {
final _ParsedCannotIgnoreData cannotIgnore;
final _ParsedEnableExperimentsData enableExperiments;
final _ParsedErrorProcessorsData errorProcessors;
final _ParsedExcludesData excludes;
final _ParsedLanguageData language;
final _ParsedLegacyPluginsData legacyPlugins;
final _ParsedOptionalChecksData optionalChecks;
factory _ParsedAnalyzerData.parse(
YamlNode? analyzer,
DiagnosticReporter reporter,
) {
if (analyzer == null || analyzer.isNullScalar) {
return _ParsedAnalyzerData._empty();
}
if (analyzer is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.analyzer)
.atSourceSpan(analyzer.span),
);
return _ParsedAnalyzerData._empty();
}
_reportUnsupportedSectionKeys(
section: analyzer,
sectionName: AnalysisOptionsFileKeys.analyzer,
supportedOptions: AnalysisOptionsFileKeys.analyzerOptions,
reporter: reporter,
);
_ParsedStrongModeData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.strongMode),
reporter,
);
if (analyzer.getKey(AnalysisOptionsFileKeys.plugins) case var keyNode?) {
reporter.report(
diag.analysisOptionsDeprecatedPlugins.atSourceSpan(keyNode.span),
);
}
return _ParsedAnalyzerData._(
cannotIgnore: _ParsedCannotIgnoreData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.cannotIgnore),
reporter,
),
enableExperiments: _ParsedEnableExperimentsData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.enableExperiment),
reporter,
),
errorProcessors: _ParsedErrorProcessorsData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.errors),
reporter,
),
excludes: _ParsedExcludesData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.exclude),
reporter,
),
language: _ParsedLanguageData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.language),
reporter,
),
legacyPlugins: _ParsedLegacyPluginsData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.plugins),
),
optionalChecks: _ParsedOptionalChecksData.parse(
analyzer.valueAt(AnalysisOptionsFileKeys.optionalChecks),
reporter,
),
);
}
_ParsedAnalyzerData._({
required this.cannotIgnore,
required this.enableExperiments,
required this.errorProcessors,
required this.excludes,
required this.language,
required this.legacyPlugins,
required this.optionalChecks,
});
factory _ParsedAnalyzerData._empty() {
return _ParsedAnalyzerData._(
cannotIgnore: _ParsedCannotIgnoreData._(),
enableExperiments: _ParsedEnableExperimentsData._(),
errorProcessors: _ParsedErrorProcessorsData._(),
excludes: _ParsedExcludesData._(),
language: _ParsedLanguageData._(),
legacyPlugins: _ParsedLegacyPluginsData._(),
optionalChecks: _ParsedOptionalChecksData._(),
);
}
}
/// Local analyzer cannot-ignore semantics computed from one parsed options file.
final class _ParsedCannotIgnoreData {
final List<String>? names;
factory _ParsedCannotIgnoreData.parse(
YamlNode? cannotIgnore,
DiagnosticReporter reporter,
) {
if (cannotIgnore == null) {
return _ParsedCannotIgnoreData._();
}
if (cannotIgnore is! YamlList) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.cannotIgnore)
.atSourceSpan(cannotIgnore.span),
);
return _ParsedCannotIgnoreData._();
}
var lintCodeNames = _DiagnosticOptions.currentLintCodeNames();
var listedNames = <String>{};
var names = <String>[];
for (var nameNode in cannotIgnore.nodes) {
if (nameNode case YamlScalar(value: String name)) {
names.add(name);
if (AnalysisOptionsFileKeys.severities.contains(name)) {
listedNames.add(name);
continue;
}
var upperCaseName = name.toUpperCase();
if (!_DiagnosticOptions.isKnownDiagnosticOrLintCode(
upperCaseName,
lintCodeNames,
)) {
reporter.report(
diag.unrecognizedErrorCode
.withArguments(codeName: name)
.atSourceSpan(nameNode.span),
);
} else if (listedNames.contains(upperCaseName)) {
// TODO(srawlins): Create a "duplicate value" code and report it.
} else {
listedNames.add(upperCaseName);
}
} else {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.cannotIgnore)
.atSourceSpan(nameNode.span),
);
}
}
return _ParsedCannotIgnoreData._(names: names);
}
_ParsedCannotIgnoreData._({this.names});
}
/// Local code-style semantics computed from one parsed options file.
final class _ParsedCodeStyleData {
final bool? useFormatter;
factory _ParsedCodeStyleData.parse(
YamlNode? codeStyle,
DiagnosticReporter reporter,
) {
if (codeStyle == null || codeStyle.isNullScalar) {
return _ParsedCodeStyleData._();
}
if (codeStyle is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.codeStyle)
.atSourceSpan(codeStyle.span),
);
return _ParsedCodeStyleData._();
}
bool? useFormatter;
for (var (:keyNode, :valueNode) in codeStyle.nodeEntries) {
switch (keyNode.value) {
case AnalysisOptionsFileKeys.format:
if (valueNode is YamlScalar) {
useFormatter = valueNode.toBool();
if (useFormatter == null) {
reporter.report(
diag.unsupportedValue
.withArguments(
optionName: AnalysisOptionsFileKeys.format,
invalidValue: valueNode.value.toString(),
legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal,
)
.atSourceSpan(valueNode.span),
);
}
} else {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.format)
.atSourceSpan(valueNode.span),
);
}
default:
reporter.report(
diag.unsupportedOptionWithoutValues
.withArguments(
sectionName: AnalysisOptionsFileKeys.codeStyle,
optionKey: keyNode.toString(),
)
.atSourceSpan(keyNode.span),
);
}
}
return _ParsedCodeStyleData._(useFormatter: useFormatter);
}
_ParsedCodeStyleData._({this.useFormatter});
}
/// Local analyzer enable-experiment semantics computed from one parsed options file.
final class _ParsedEnableExperimentsData {
final List<String>? flags;
factory _ParsedEnableExperimentsData.parse(
YamlNode? enableExperiments,
DiagnosticReporter reporter,
) {
if (enableExperiments == null) {
return _ParsedEnableExperimentsData._();
}
if (enableExperiments is! YamlList) {
reporter.report(
diag.invalidSectionFormat
.withArguments(
sectionName: AnalysisOptionsFileKeys.enableExperiment,
)
.atSourceSpan(enableExperiments.span),
);
return _ParsedEnableExperimentsData._();
}
var flags = <String>[];
var flagSpans = <SourceSpan>[];
for (var node in enableExperiments.nodes) {
if (node case YamlScalar(value: String flag)) {
flags.add(flag);
flagSpans.add(node.span);
} else {
reporter.report(
diag.invalidSectionFormat
.withArguments(
sectionName: AnalysisOptionsFileKeys.enableExperiment,
)
.atSourceSpan(node.span),
);
}
}
for (var validationResult in validateFlags(flags)) {
var flagIndex = validationResult.stringIndex;
var span = flagSpans[flagIndex];
if (validationResult is UnrecognizedFlag) {
reporter.report(
diag.unsupportedOptionWithoutValues
.withArguments(
sectionName: AnalysisOptionsFileKeys.enableExperiment,
optionKey: flags[flagIndex],
)
.atSourceSpan(span),
);
} else {
reporter.report(
diag.invalidOption
.withArguments(
optionName: AnalysisOptionsFileKeys.enableExperiment,
detailMessage: validationResult.message,
)
.atSourceSpan(span),
);
}
}
return _ParsedEnableExperimentsData._(flags: flags);
}
_ParsedEnableExperimentsData._({this.flags});
}
/// Local analyzer error-processor semantics computed from one parsed options file.
final class _ParsedErrorProcessorsData {
final List<ErrorProcessor>? processors;
factory _ParsedErrorProcessorsData.parse(
YamlNode? errors,
DiagnosticReporter reporter,
) {
if (errors == null) {
return _ParsedErrorProcessorsData._();
}
if (errors is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.errors)
.atSourceSpan(errors.span),
);
return _ParsedErrorProcessorsData._();
}
var lintCodeNames = _DiagnosticOptions.currentLintCodeNames();
var processors = <ErrorProcessor>[];
for (var (:keyNode, :valueNode) in errors.nodeEntries) {
String code;
switch (keyNode) {
case YamlScalar(value: String keyValue):
code = keyValue;
var upperCaseCode = keyValue.toUpperCase();
if (!_DiagnosticOptions.isKnownDiagnosticOrLintCode(
upperCaseCode,
lintCodeNames,
)) {
reporter.report(
diag.unrecognizedErrorCode
.withArguments(codeName: keyValue)
.atSourceSpan(keyNode.span),
);
}
case YamlScalar():
reporter.report(
diag.unrecognizedErrorCode
.withArguments(codeName: keyNode.value.toString())
.atSourceSpan(keyNode.span),
);
continue;
default:
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.errors)
.atSourceSpan(keyNode.span),
);
continue;
}
if (valueNode is! YamlScalar) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.errors)
.atSourceSpan(valueNode.span),
);
continue;
}
var action = valueNode.value.toString().toLowerCase();
if (!_DiagnosticOptions.errorProcessorLegalValues.contains(action)) {
reporter.report(
diag.unsupportedOptionWithLegalValues
.withArguments(
sectionName: AnalysisOptionsFileKeys.errors,
optionKey: valueNode.value.toString(),
legalValues: _DiagnosticOptions
.errorProcessorLegalValues
.quotedAndCommaSeparatedWithAnd,
)
.atSourceSpan(valueNode.span),
);
continue;
}
if (AnalysisOptionsFileKeys.ignoreSynonyms.contains(action)) {
processors.add(ErrorProcessor.ignore(code));
} else if (severityMap[action] case var severity?) {
processors.add(ErrorProcessor(code, severity));
}
}
return _ParsedErrorProcessorsData._(processors: processors);
}
_ParsedErrorProcessorsData._({this.processors});
}
/// Local analyzer exclude semantics computed from one parsed options file.
final class _ParsedExcludesData {
final List<String>? patterns;
factory _ParsedExcludesData.parse(
YamlNode? excludes,
DiagnosticReporter reporter,
) {
if (excludes == null) {
return _ParsedExcludesData._();
}
if (excludes is! YamlList) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.exclude)
.atSourceSpan(excludes.span),
);
return _ParsedExcludesData._();
}
var patterns = <String>[];
for (var node in excludes.nodes) {
if (node.stringValue case var pattern?) {
patterns.add(pattern);
} else {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.exclude)
.atSourceSpan(node.span),
);
}
}
return _ParsedExcludesData._(patterns: patterns);
}
_ParsedExcludesData._({this.patterns});
}
/// Local facts computed from one parsed options file.
final class _ParsedFileData {
final File file;
final List<Diagnostic> diagnostics;
final _ParsedAnalyzerData analyzer;
final _ParsedCodeStyleData codeStyle;
final _ParsedFormatterData formatter;
final _ParsedLinterData linter;
final _ParsedPluginsData plugins;
_ParsedFileData({
required this.file,
required this.diagnostics,
required this.analyzer,
required this.codeStyle,
required this.formatter,
required this.linter,
required this.plugins,
});
factory _ParsedFileData.parse(
_ParsedFileNode file, {
required Folder contextRoot,
required VersionConstraint? sdkVersionConstraint,
required bool isInitialFile,
}) {
var diagnosticListener = RecordingDiagnosticListener();
var reporter = DiagnosticReporter(
diagnosticListener,
FileSource(file.file),
);
var yamlMap = file.content.yaml.tryCast<YamlMap>();
var analyzer = _ParsedAnalyzerData.parse(
yamlMap?.valueAt(AnalysisOptionsFileKeys.analyzer),
reporter,
);
var codeStyle = _ParsedCodeStyleData.parse(
yamlMap?.valueAt(AnalysisOptionsFileKeys.codeStyle),
reporter,
);
var formatter = _ParsedFormatterData.parse(
yamlMap?.valueAt(AnalysisOptionsFileKeys.formatter),
reporter,
);
var linter = _ParsedLinterData.parse(
yamlMap,
file: file.file,
sdkVersionConstraint: sdkVersionConstraint,
isInitialFile: isInitialFile,
reporter: reporter,
);
var plugins = _ParsedPluginsData.parse(
yamlMap?.valueAt(AnalysisOptionsFileKeys.plugins),
file: file.file,
contextRoot: contextRoot,
isInitialFile: isInitialFile,
reporter: reporter,
);
return _ParsedFileData(
file: file.file,
diagnostics: diagnosticListener.diagnostics,
analyzer: analyzer,
codeStyle: codeStyle,
formatter: formatter,
linter: linter,
plugins: plugins,
);
}
_LocalLinterRules get localLinterRules => linter.localRules;
}
/// A successfully parsed file node in the include graph.
///
/// This is an internal file-based model shared by effective options
/// construction and include walking.
final class _ParsedFileNode extends _FileNode {
@override
final AnalysisOptionsFileContent content;
/// The resolved meaning of each valid `include` directive in [content].
///
/// This is assigned by [AnalysisOptionsParseSession] after the node is in
/// the parse cache, so recursive includes can point back at the same node.
late final List<_IncludeResolution> includeResolutions;
_ParsedFileNode({required super.file, required this.content});
}
/// Effective semantics for a parsed file and its includes.
///
/// [localData] holds diagnostics and values from the file itself. The remaining
/// fields describe values inherited from includes and the final values after
/// local options override included options.
final class _ParsedFileSemantics {
final _ParsedFileData localData;
final List<_IncludedLinterRules> includedLinterRules;
final _EffectiveLinterRules includedEffectiveLinterRules;
final _EffectiveLinterRules effectiveLinterRules;
/// The legacy plugin node after merging included files.
final YamlNode? includedLegacyPluginsNode;
_ParsedFileSemantics({
required this.localData,
required this.includedLinterRules,
required this.includedEffectiveLinterRules,
required this.effectiveLinterRules,
required this.includedLegacyPluginsNode,
});
/// The legacy plugin node after applying local options over includes.
YamlNode? get effectiveLegacyPluginsNode {
var includedNode = includedLegacyPluginsNode;
var localNode = localData.analyzer.legacyPlugins.node;
if (includedNode == null) {
return localNode;
}
if (localNode == null) {
return includedNode;
}
// TODO(scheglov): why?
return Merger().merge(includedNode, localNode);
}
/// The first legacy plugin name found while walking included files.
String? get includedLegacyPluginName {
if (includedLegacyPluginsNode case var node?) {
return _ParsedLegacyPluginsData.parse(node).firstPluginName;
}
return null;
}
}
/// Local formatter semantics computed from one parsed options file.
final class _ParsedFormatterData {
final int? pageWidth;
final TrailingCommas? trailingCommas;
factory _ParsedFormatterData.parse(
YamlNode? formatter,
DiagnosticReporter reporter,
) {
if (formatter == null || formatter.isNullScalar) {
return _ParsedFormatterData._();
}
if (formatter is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.formatter)
.atSourceSpan(formatter.span),
);
return _ParsedFormatterData._();
}
void reportInvalidPageWidth(YamlNode valueNode) {
reporter.report(
diag.invalidOption
.withArguments(
optionName: AnalysisOptionsFileKeys.pageWidth,
detailMessage: '"page_width" must be a positive integer.',
)
.atSourceSpan(valueNode.span),
);
}
void reportInvalidTrailingCommas(YamlNode valueNode) {
reporter.report(
diag.invalidOption
.withArguments(
optionName: AnalysisOptionsFileKeys.trailingCommas,
detailMessage:
'"trailing_commas" must be "automate" or "preserve".',
)
.atSourceSpan(valueNode.span),
);
}
int? pageWidth;
TrailingCommas? trailingCommas;
for (var (:keyNode, :valueNode) in formatter.nodeEntries) {
switch (keyNode.value) {
case AnalysisOptionsFileKeys.pageWidth:
Object? value = valueNode.value;
if (value case int value when value > 0) {
pageWidth = value;
} else {
reportInvalidPageWidth(valueNode);
}
case AnalysisOptionsFileKeys.trailingCommas:
Object? value = valueNode.value;
trailingCommas = TrailingCommas.values.asNameMap()[value];
if (trailingCommas == null) {
reportInvalidTrailingCommas(valueNode);
}
default:
reporter.report(
diag.unsupportedOptionWithoutValues
.withArguments(
sectionName: AnalysisOptionsFileKeys.formatter,
optionKey: keyNode.toString(),
)
.atSourceSpan(keyNode.span),
);
}
}
return _ParsedFormatterData._(
pageWidth: pageWidth,
trailingCommas: trailingCommas,
);
}
_ParsedFormatterData._({this.pageWidth, this.trailingCommas});
}
/// An `include` directive whose target was parsed successfully.
final class _ParsedInclude extends _IncludeResolution {
final _ParsedFileNode file;
_ParsedInclude({required super.include, required this.file});
}
/// Local analyzer language semantics computed from one parsed options file.
final class _ParsedLanguageData {
final bool? strictCasts;
final bool? strictInference;
final bool? strictRawTypes;
factory _ParsedLanguageData.parse(
YamlNode? language,
DiagnosticReporter reporter,
) {
if (language == null || language.isNullScalar) {
return _ParsedLanguageData._();
}
if (language is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.language)
.atSourceSpan(language.span),
);
return _ParsedLanguageData._();
}
var unsupportedKey = _ErrorBuilder(AnalysisOptionsFileKeys.languageOptions);
bool? parseBool(String optionName, YamlNode valueNode) {
if (valueNode is! YamlScalar) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: optionName)
.atSourceSpan(valueNode.span),
);
return null;
}
var value = valueNode.toBool();
if (value == null) {
reporter.report(
diag.unsupportedValue
.withArguments(
optionName: optionName,
invalidValue: valueNode.value.toString(),
legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal,
)
.atSourceSpan(valueNode.span),
);
}
return value;
}
bool? strictCasts;
bool? strictInference;
bool? strictRawTypes;
for (var (:keyNode, :valueNode) in language.nodeEntries) {
if (keyNode is! YamlScalar) {
continue;
}
switch (keyNode.value) {
case AnalysisOptionsFileKeys.strictCasts:
strictCasts = parseBool(
AnalysisOptionsFileKeys.strictCasts,
valueNode,
);
case AnalysisOptionsFileKeys.strictInference:
strictInference = parseBool(
AnalysisOptionsFileKeys.strictInference,
valueNode,
);
case AnalysisOptionsFileKeys.strictRawTypes:
strictRawTypes = parseBool(
AnalysisOptionsFileKeys.strictRawTypes,
valueNode,
);
default:
unsupportedKey.reportError(
reporter,
AnalysisOptionsFileKeys.language,
keyNode,
);
}
}
return _ParsedLanguageData._(
strictCasts: strictCasts,
strictInference: strictInference,
strictRawTypes: strictRawTypes,
);
}
_ParsedLanguageData._({
this.strictCasts,
this.strictInference,
this.strictRawTypes,
});
}
/// Local legacy `analyzer/plugins` semantics computed from one parsed options file.
final class _ParsedLegacyPluginsData {
final YamlNode? node;
final String? firstPluginName;
final List<_LegacyPluginData> entries;
final List<_LegacyPluginData> localConflicts;
factory _ParsedLegacyPluginsData.parse(YamlNode? plugins) {
return switch (plugins) {
null => _ParsedLegacyPluginsData._(),
YamlScalar(value: null) => _ParsedLegacyPluginsData._(node: plugins),
YamlScalar() => _ParsedLegacyPluginsData._fromNodes(plugins, [plugins]),
YamlList() => _ParsedLegacyPluginsData._fromNodes(plugins, plugins.nodes),
YamlMap() => _ParsedLegacyPluginsData._fromNodes(
plugins,
plugins.nodes.keys.cast<YamlNode?>(),
),
_ => _ParsedLegacyPluginsData._(node: plugins),
};
}
_ParsedLegacyPluginsData._({
this.node,
this.firstPluginName,
this.entries = const [],
this.localConflicts = const [],
});
factory _ParsedLegacyPluginsData._fromNodes(
YamlNode node,
Iterable<YamlNode?> nodes,
) {
var entries = <_LegacyPluginData>[];
var localConflicts = <_LegacyPluginData>[];
String? firstPluginName;
for (var node in nodes) {
if (node == null) {
continue;
}
if (node.stringValue case var pluginName?) {
var entry = _LegacyPluginData(node: node, name: pluginName);
entries.add(entry);
if (firstPluginName == null) {
firstPluginName = pluginName;
} else if (pluginName != firstPluginName) {
localConflicts.add(entry);
}
}
}
return _ParsedLegacyPluginsData._(
node: node,
firstPluginName: firstPluginName,
entries: entries,
localConflicts: localConflicts,
);
}
void reportLocalMultiplePlugins(DiagnosticReporter reporter) {
if (firstPluginName case var firstPluginName?) {
for (var conflict in localConflicts) {
_reportMultiplePlugins(reporter, conflict.node, firstPluginName);
}
}
}
void reportMultiplePluginsWithIncluded(
DiagnosticReporter reporter, {
required String includedPluginName,
}) {
for (var entry in entries) {
if (entry.name != includedPluginName) {
_reportMultiplePlugins(reporter, entry.node, includedPluginName);
}
}
}
void _reportMultiplePlugins(
DiagnosticReporter reporter,
YamlNode node,
String firstPluginName,
) {
reporter.report(
diag.multiplePlugins
.withArguments(firstPluginName: firstPluginName)
.atSourceSpan(node.span),
);
}
}
/// Local runtime linter configuration computed from one parsed options file.
final class _ParsedLinterData {
static const _falseValue = 'false';
static const _ignoreValue = 'ignore';
static const _trueValue = 'true';
static const _infoValue = 'info';
static const _warningValue = 'warning';
static const _errorValue = 'error';
static const _validLintStringValues = [
_ignoreValue,
_infoValue,
_warningValue,
_errorValue,
];
static const _validLintValues = [
_trueValue,
_falseValue,
..._validLintStringValues,
];
final _ParsedLinterDataKind kind;
final Map<String, RuleConfig> ruleConfigs;
final _LocalLinterRules localRules;
factory _ParsedLinterData.absent() {
return _ParsedLinterData._(
_ParsedLinterDataKind.absent,
const {},
_LocalLinterRules.empty,
);
}
factory _ParsedLinterData.invalid() {
return _ParsedLinterData._(
_ParsedLinterDataKind.invalid,
const {},
_LocalLinterRules.empty,
);
}
factory _ParsedLinterData.parse(
YamlMap? yamlMap, {
required File file,
required VersionConstraint? sdkVersionConstraint,
required bool isInitialFile,
required DiagnosticReporter reporter,
}) {
if (yamlMap == null) {
return _ParsedLinterData.absent();
}
var linter = yamlMap.valueAt(AnalysisOptionsFileKeys.linter);
if (linter == null || linter.isNullScalar) {
return _ParsedLinterData.absent();
}
if (linter is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.linter)
.atSourceSpan(linter.span),
);
return _ParsedLinterData.invalid();
}
var rules = linter.valueAt(AnalysisOptionsFileKeys.rules);
var localRules = _parseLocalRules(
rules,
file: file,
sdkVersionConstraint: sdkVersionConstraint,
isInitialFile: isInitialFile,
reporter: reporter,
);
_reportUnsupportedSectionKeys(
section: linter,
sectionName: AnalysisOptionsFileKeys.linter,
supportedOptions: AnalysisOptionsFileKeys.linterOptions,
reporter: reporter,
);
return _ParsedLinterData.valid(
// TODO(scheglov): Compute rule configs in this parser pass too. This
// still delegates to the shared linter helper because it is used outside
// analyzer options parsing, including by pkg/linter.
parseLinterSection(yamlMap) ?? const {},
localRules: localRules,
);
}
factory _ParsedLinterData.valid(
Map<String, RuleConfig> ruleConfigs, {
required _LocalLinterRules localRules,
}) {
return _ParsedLinterData._(
_ParsedLinterDataKind.valid,
ruleConfigs,
localRules,
);
}
_ParsedLinterData._(this.kind, this.ruleConfigs, this.localRules);
static bool _beforeCurrentConstraint(
Version? since,
VersionConstraint? sdkVersionConstraint,
) {
// No "since" applies to all SDKs.
if (since == null) return true;
return switch (sdkVersionConstraint) {
VersionRange(min: var min?) => since <= min,
_ => false,
};
}
static AbstractAnalysisRule? _getRegisteredLint(String value) =>
Registry.ruleRegistry[value];
static bool _isDeprecatedInCurrentOrEarlierSdk(
RuleState state,
VersionConstraint? sdkVersionConstraint,
) =>
state.isDeprecated &&
_beforeCurrentConstraint(state.since, sdkVersionConstraint);
static bool _isRemovedInCurrentOrEarlierSdk(
RuleState state,
VersionConstraint? sdkVersionConstraint,
) =>
state.isRemoved &&
_beforeCurrentConstraint(state.since, sdkVersionConstraint);
static _LocalLinterRules _parseLocalRules(
YamlNode? rules, {
required File file,
required VersionConstraint? sdkVersionConstraint,
required bool isInitialFile,
required DiagnosticReporter reporter,
}) {
if (rules is! YamlList &&
rules is! YamlMap &&
// This handles empty keys like
// linter:
// rules:
(rules is! YamlScalar || rules.value != null) &&
// We accept 'null' for triggering `INCOMPATIBLE_LINT_INCLUDED`.
rules != null) {
return _LocalLinterRules.empty;
}
_RuleData? parseRule(YamlScalar node, Object? enabled) {
var value = node.value;
if (value is! String) return null;
if (enabled == null) return null;
var rule = _getRegisteredLint(value);
if (rule == null) {
reporter.report(
diag.undefinedLint
.withArguments(ruleName: value)
.atSourceSpan(node.span),
);
return null;
}
Object? ruleValue;
bool enabledValue;
if (enabled is YamlNode) {
ruleValue = enabled.value;
} else if (enabled is bool) {
ruleValue = enabled;
enabledValue = enabled;
}
if (ruleValue == null) {
return null;
}
if (ruleValue is String && _validLintStringValues.contains(ruleValue)) {
enabledValue = ruleValue != _ignoreValue;
} else if (ruleValue is bool) {
enabledValue = ruleValue;
} else {
enabledValue = false;
var warningNode = enabled is YamlNode ? enabled : node;
reporter.report(
diag.unsupportedValue
.withArguments(
optionName: value.toString(),
invalidValue: ruleValue.toString(),
legalValues: _validLintValues.quotedAndCommaSeparatedWithOr,
)
.atSourceSpan(warningNode.span),
);
}
// Report removed or deprecated lint warnings defined directly (and not
// in includes).
if (isInitialFile) {
var state = rule.state;
if (_isDeprecatedInCurrentOrEarlierSdk(state, sdkVersionConstraint)) {
var replacedBy = state.replacedBy;
if (replacedBy != null) {
reporter.report(
diag.deprecatedLintWithReplacement
.withArguments(
deprecatedRuleName: value,
replacementRuleName: replacedBy,
)
.atSourceSpan(node.span),
);
} else {
reporter.report(
diag.deprecatedLint
.withArguments(ruleName: value)
.atSourceSpan(node.span),
);
}
} else if (_isRemovedInCurrentOrEarlierSdk(
state,
sdkVersionConstraint,
)) {
var since = state.since.toString();
var replacedBy = state.replacedBy;
if (replacedBy != null) {
reporter.report(
diag.replacedLint
.withArguments(
ruleName: value,
sdkVersion: since,
replacingLintName: replacedBy,
)
.atSourceSpan(node.span),
);
} else {
reporter.report(
diag.removedLint
.withArguments(ruleName: value, sdkVersion: since)
.atSourceSpan(node.span),
);
}
}
}
return _RuleData(rule, node, file: file, isEnabled: enabledValue);
}
var activeRules = <AbstractAnalysisRule, _RuleData>{};
var disabledRules = <_RuleData>[];
var enabledRules = <_RuleData>[];
void parseEntry(YamlNode key, Object? value) {
if (key is! YamlScalar) {
return;
}
var rule = parseRule(key, value);
if (rule == null) {
return;
}
if (rule.isEnabled) {
enabledRules.add(rule);
} else {
disabledRules.add(rule);
}
}
switch (rules) {
case YamlList(:var nodes):
for (var rule in nodes) {
parseEntry(rule, true);
}
case YamlMap():
for (var (:keyNode, :valueNode) in rules.nodeEntries) {
parseEntry(keyNode, valueNode);
}
}
for (var rule in enabledRules) {
_processEnabledRule(
ruleData: rule,
reporter: reporter,
activeRules: activeRules,
);
activeRules[rule.rule] = rule;
}
return _LocalLinterRules(enabled: enabledRules, disabled: disabledRules);
}
/// Processes an enabled rule by checking for incompatible rules and reporting
/// any issues found.
static void _processEnabledRule({
required _RuleData ruleData,
required Map<AbstractAnalysisRule, _RuleData> activeRules,
required DiagnosticReporter reporter,
}) {
String value = ruleData.node.value.toString();
var incompatible = _LinterRuleDiagnostics.findIncompatibleRules(
ruleData.rule,
rules: activeRules.values,
);
if (incompatible.isNotEmpty) {
reporter.report(
_LinterRuleDiagnostics.diagnosticFactory.incompatibleLint(
source: reporter.source,
reference: ruleData.node,
incompatibleRules: {
for (var data in incompatible) data.file.path: data.node,
},
),
);
}
if (activeRules.containsKey(ruleData.rule)) {
reporter.report(
diag.duplicateRule
.withArguments(ruleName: value)
.atSourceSpan(ruleData.node.span),
);
}
}
}
enum _ParsedLinterDataKind { absent, invalid, valid }
/// Local analyzer optional-checks semantics computed from one parsed options file.
final class _ParsedOptionalChecksData {
final bool? chromeOsManifestChecks;
final bool? propagateLinterExceptions;
factory _ParsedOptionalChecksData.parse(
YamlNode? optionalChecks,
DiagnosticReporter reporter,
) {
void reportUnsupportedKey(YamlNode node) {
var unsupportedKey = _ErrorBuilder(
AnalysisOptionsFileKeys.optionalChecksOptions,
);
unsupportedKey.reportError(
reporter,
AnalysisOptionsFileKeys
.optionalChecksOptions
.quotedAndCommaSeparatedWithOr,
node,
);
}
bool? parseBool(String optionName, YamlNode valueNode) {
if (valueNode is! YamlScalar) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: optionName)
.atSourceSpan(valueNode.span),
);
return null;
}
if (valueNode.toBool() case var value?) {
return value;
} else {
reporter.report(
diag.unsupportedValue
.withArguments(
optionName: optionName,
invalidValue: valueNode.value.toString(),
legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal,
)
.atSourceSpan(valueNode.span),
);
}
return null;
}
bool? chromeOsManifestChecks;
bool? propagateLinterExceptions;
switch (optionalChecks) {
case null:
break;
case YamlScalar():
switch (optionalChecks.value) {
case AnalysisOptionsFileKeys.chromeOsManifestChecks:
chromeOsManifestChecks = true;
case AnalysisOptionsFileKeys.propagateLinterExceptions:
propagateLinterExceptions = true;
default:
reportUnsupportedKey(optionalChecks);
}
case YamlMap():
for (var (:keyNode, :valueNode) in optionalChecks.nodeEntries) {
if (keyNode is! YamlScalar) {
continue;
}
switch (keyNode.value) {
case AnalysisOptionsFileKeys.chromeOsManifestChecks:
chromeOsManifestChecks = parseBool(
AnalysisOptionsFileKeys.chromeOsManifestChecks,
valueNode,
);
case AnalysisOptionsFileKeys.propagateLinterExceptions:
propagateLinterExceptions = parseBool(
AnalysisOptionsFileKeys.propagateLinterExceptions,
valueNode,
);
default:
reportUnsupportedKey(keyNode);
}
}
default:
reporter.report(
diag.invalidSectionFormat
.withArguments(
sectionName: AnalysisOptionsFileKeys.optionalChecks,
)
.atSourceSpan(optionalChecks.span),
);
}
return _ParsedOptionalChecksData._(
chromeOsManifestChecks: chromeOsManifestChecks,
propagateLinterExceptions: propagateLinterExceptions,
);
}
_ParsedOptionalChecksData._({
this.chromeOsManifestChecks,
this.propagateLinterExceptions,
});
}
/// Local top-level `plugins` semantics computed from one parsed options file.
final class _ParsedPluginsData {
// TODO(scheglov): Decide whether an invalid `plugins` section should keep
// included plugin options instead of clearing them; this preserves existing
// behavior while plugin parsing and validation are being unified.
final bool clearsExisting;
final List<PluginConfiguration> configurations;
final Map<String, PluginSource>? dependencyOverrides;
factory _ParsedPluginsData.parse(
YamlNode? plugins, {
required File file,
required Folder contextRoot,
required bool isInitialFile,
required DiagnosticReporter reporter,
}) {
if (plugins == null) {
return _ParsedPluginsData._();
}
if (plugins is! YamlMap) {
switch (plugins) {
case YamlScalar(value: null):
break;
default:
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.plugins)
.atSourceSpan(plugins.span),
);
}
return _ParsedPluginsData._(clearsExisting: true);
}
if (file.parent != contextRoot && isInitialFile) {
reporter.report(
diag.pluginsInInnerOptions
.withArguments(contextRoot: contextRoot.path)
.atSourceSpan(plugins.span),
);
}
var configurations = <PluginConfiguration>[];
Map<String, PluginSource>? dependencyOverrides;
for (var (keyNode: nameNode, valueNode: pluginNode)
in plugins.nodeEntries) {
if (nameNode is! YamlScalar) {
continue;
}
var pluginName = nameNode.toString();
if (pluginName == AnalysisOptionsFileKeys.dependencyOverrides) {
if (pluginNode is YamlMap) {
for (var (keyNode: overrideNameNode, valueNode: overrideNode)
in pluginNode.nodeEntries) {
if (overrideNameNode is! YamlScalar) {
continue;
}
if (_parsePluginSource(overrideNode, file) case var source?) {
(dependencyOverrides ??= {})[overrideNameNode.toString()] =
source;
}
if (overrideNameNode.value is String) {
_validatePlugin(
reporter,
'${AnalysisOptionsFileKeys.plugins}/${AnalysisOptionsFileKeys.dependencyOverrides}',
overrideNameNode.toString(),
overrideNode,
);
}
}
} else {
reporter.report(
diag.invalidSectionFormat
.withArguments(
sectionName:
'${AnalysisOptionsFileKeys.plugins}/${AnalysisOptionsFileKeys.dependencyOverrides}',
)
.atSourceSpan(pluginNode.span),
);
}
continue;
}
if (_parsePluginSource(pluginNode, file) case var source?) {
Map<String, RuleConfig> diagnosticConfigurations = const {};
if (pluginNode is YamlMap) {
var diagnostics = pluginNode.valueAt(
AnalysisOptionsFileKeys.diagnostics,
);
if (diagnostics != null) {
diagnosticConfigurations = parseDiagnosticsSection(diagnostics);
}
}
configurations.add(
PluginConfiguration(
name: pluginName,
source: source,
diagnosticConfigs: diagnosticConfigurations,
// TODO(srawlins): Implement `enabled: false`.
),
);
}
if (nameNode.value is String) {
_validatePlugin(
reporter,
AnalysisOptionsFileKeys.plugins,
pluginName,
pluginNode,
);
}
}
return _ParsedPluginsData._(
configurations: configurations,
dependencyOverrides: dependencyOverrides,
);
}
_ParsedPluginsData._({
this.clearsExisting = false,
this.configurations = const [],
this.dependencyOverrides,
});
static PluginSource? _parsePluginSource(YamlNode pluginNode, File file) {
if (pluginNode case YamlScalar(:String value)) {
return VersionedPluginSource(constraint: value);
}
if (pluginNode is! YamlMap) {
return null;
}
// Grab either the source value from 'version', 'git', or 'path'. In the
// erroneous case that multiple are specified, just take the first. A
// warning should be reported by the options validation path.
// TODO(srawlins): In addition to 'version' and 'path', try 'git'.
var versionSource = pluginNode.valueAt(AnalysisOptionsFileKeys.version);
var hostedUrlSource = pluginNode.valueAt(AnalysisOptionsFileKeys.hosted);
if ((versionSource, hostedUrlSource) case (
YamlScalar(value: String version),
YamlScalar(value: String hostedUrl),
)) {
return VersionedPluginSource(constraint: version, hostedUrl: hostedUrl);
} else if (versionSource case YamlScalar(value: String version)) {
return VersionedPluginSource(constraint: version);
}
var gitSource = pluginNode.valueAt(AnalysisOptionsFileKeys.git);
if (gitSource case YamlScalar(:String value)) {
return GitPluginSource(url: value);
} else if (gitSource is YamlMap) {
var urlSource = gitSource.valueAt(AnalysisOptionsFileKeys.url);
if (urlSource case YamlScalar(:String value)) {
return GitPluginSource(
url: value,
path: gitSource.valueAt(AnalysisOptionsFileKeys.path).stringValue,
ref: gitSource.valueAt(AnalysisOptionsFileKeys.ref).stringValue,
tagPattern: gitSource
.valueAt(AnalysisOptionsFileKeys.tagPattern)
.stringValue,
);
}
}
var pathSource = pluginNode.valueAt(AnalysisOptionsFileKeys.path);
if (pathSource case YamlScalar(value: String pathValue)) {
var pathContext = file.provider.pathContext;
if (pathContext.isRelative(pathValue)) {
// The plugin source is later used in a synthetic pub package, so store
// an absolute path before the source leaves options parsing.
pathValue = pathContext.join(file.parent.path, pathValue);
pathValue = pathContext.normalize(pathValue);
}
return PathPluginSource(path: pathValue);
}
return null;
}
static void _validateDiagnostics(
DiagnosticReporter reporter,
String pluginPath,
YamlNode diagnosticsValue,
) {
var sectionName = [
pluginPath,
AnalysisOptionsFileKeys.diagnostics,
].join('/');
if (diagnosticsValue is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: sectionName)
.atSourceSpan(diagnosticsValue.span),
);
return;
}
for (var (keyNode: _, valueNode: severityNode)
in diagnosticsValue.nodeEntries) {
// The keys are diagnostic codes.
if (severityNode is! YamlScalar) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: sectionName)
.atSourceSpan(severityNode.span),
);
return;
}
var severity = severityNode.value?.toString().toLowerCase();
if (!_DiagnosticOptions.errorProcessorLegalValues.contains(severity)) {
reporter.report(
diag.unsupportedOptionWithLegalValues
.withArguments(
sectionName: sectionName,
optionKey: severityNode.value.toString(),
legalValues: _DiagnosticOptions
.errorProcessorLegalValues
.quotedAndCommaSeparatedWithAnd,
)
.atSourceSpan(severityNode.span),
);
}
}
}
static void _validateGit(
DiagnosticReporter reporter,
String pluginPath,
YamlNode gitValue,
) {
var sectionName = [pluginPath, AnalysisOptionsFileKeys.git].join('/');
if (gitValue is YamlScalar) {
if (gitValue.value is! String) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: sectionName)
.atSourceSpan(gitValue.span),
);
}
return;
}
if (gitValue is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: sectionName)
.atSourceSpan(gitValue.span),
);
return;
}
var unsupportedGitOption = _ErrorBuilder(
AnalysisOptionsFileKeys.gitOptions,
);
for (var (:keyNode, :valueNode) in gitValue.nodeEntries) {
if (keyNode case YamlScalar(value: String key)) {
if (!AnalysisOptionsFileKeys.gitOptions.contains(key)) {
unsupportedGitOption.reportError(reporter, sectionName, keyNode);
} else if (valueNode is! YamlScalar || valueNode.value is! String) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: '$sectionName/$key')
.atSourceSpan(valueNode.span),
);
}
}
}
}
static void _validatePlugin(
DiagnosticReporter reporter,
String parentPath,
String pluginName,
YamlNode pluginValue,
) {
var pluginPath = '$parentPath/$pluginName';
switch (pluginValue) {
case YamlScalar(value: String()):
// Valid enough. We could validate that it is a legal VersionConstraint
// from the pub_semver package.
break;
case YamlMap():
_validatePluginMap(reporter, pluginPath, pluginValue);
default:
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: pluginPath)
.atSourceSpan(pluginValue.span),
);
}
}
static void _validatePluginMap(
DiagnosticReporter reporter,
String pluginPath,
YamlMap pluginValue,
) {
var unsupportedPluginOption = _ErrorBuilder(
AnalysisOptionsFileKeys.pluginsOptions,
);
for (var (keyNode: pluginMapKeyNode, valueNode: pluginMapValueNode)
in pluginValue.nodeEntries) {
if (pluginMapKeyNode case YamlScalar(value: String pluginMapKey)) {
if (pluginMapKey == AnalysisOptionsFileKeys.diagnostics) {
_validateDiagnostics(reporter, pluginPath, pluginMapValueNode);
} else if (pluginMapKey == AnalysisOptionsFileKeys.git) {
_validateGit(reporter, pluginPath, pluginMapValueNode);
} else if (!AnalysisOptionsFileKeys.pluginsOptions.contains(
pluginMapKey,
)) {
unsupportedPluginOption.reportError(
reporter,
pluginPath,
pluginMapKeyNode,
);
}
}
// TODO(srawlins): Validate 'path' is a YamlScalar.
}
}
}
/// Diagnostics for the deprecated `analyzer/strong-mode` section.
final class _ParsedStrongModeData {
factory _ParsedStrongModeData.parse(
YamlNode? strongMode,
DiagnosticReporter reporter,
) {
if (strongMode == null) {
return _ParsedStrongModeData._();
}
if (strongMode is! YamlMap) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: AnalysisOptionsFileKeys.strongMode)
.atSourceSpan(strongMode.span),
);
return _ParsedStrongModeData._();
}
bool? parseBool(String optionName, YamlNode valueNode) {
if (valueNode is! YamlScalar) {
reporter.report(
diag.invalidSectionFormat
.withArguments(sectionName: optionName)
.atSourceSpan(valueNode.span),
);
return null;
}
if (valueNode.toBool() case var value?) {
return value;
} else {
reporter.report(
diag.unsupportedValue
.withArguments(
optionName: optionName,
invalidValue: valueNode.value.toString(),
legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal,
)
.atSourceSpan(valueNode.span),
);
}
return null;
}
for (var (:keyNode, :valueNode) in strongMode.nodeEntries) {
if (keyNode is! YamlScalar) {
continue;
}
var optionName = keyNode.value?.toString();
switch (optionName) {
case AnalysisOptionsFileKeys.implicitCasts:
parseBool(AnalysisOptionsFileKeys.implicitCasts, valueNode);
case AnalysisOptionsFileKeys.implicitDynamic:
parseBool(AnalysisOptionsFileKeys.implicitDynamic, valueNode);
default:
reporter.report(
diag.unsupportedOptionWithLegalValues
.withArguments(
sectionName: AnalysisOptionsFileKeys.strongMode,
optionKey: keyNode.value.toString(),
legalValues: {
AnalysisOptionsFileKeys.implicitCasts,
AnalysisOptionsFileKeys.implicitDynamic,
}.quotedAndCommaSeparatedWithAnd,
)
.atSourceSpan(keyNode.span),
);
}
}
return _ParsedStrongModeData._();
}
_ParsedStrongModeData._();
}
final class _ParsedYamlFileContent extends _FileContent {
@override
final AnalysisOptionsFileContent content;
_ParsedYamlFileContent({required super.file, required this.content});
}
final class _UnreadableFileContent extends _FileContent {
final FileSystemException exception;
_UnreadableFileContent({required super.file, required this.exception});
@override
AnalysisOptionsFileContent? get content => null;
}
/// A file node whose content could not be read.
final class _UnreadableFileNode extends _FileNode {
final FileSystemException exception;
_UnreadableFileNode({required super.file, required this.exception});
@override
AnalysisOptionsFileContent? get content => null;
}