blob: 38e7913f185014f13df21f6864f3e064c8c349ad [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.
import 'package:analyzer/analysis_rule/rule_state.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/formatter_options.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/error_processor.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/analysis_options/analysis_options.dart';
import 'package:analyzer/src/analysis_options/analysis_options_file.dart';
import 'package:analyzer/src/analysis_options/code_style_options.dart';
import 'package:analyzer/src/analysis_rule/rule_context.dart';
import 'package:analyzer/src/dart/analysis/experiments.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/diagnostic/diagnostic_factory.dart';
import 'package:analyzer/src/error/listener.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/lint/config.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer/src/util/yaml.dart';
import 'package:analyzer/src/utilities/extensions/object.dart';
import 'package:analyzer/src/utilities/extensions/string.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
part 'analysis_options_include_walker.dart';
part 'analysis_options_parse_model.dart';
part 'linter_rule_options_validator.dart';
/// The result of parsing an analysis options file at a user-visible boundary.
///
/// Analysis options are configuration, so parsing is best-effort: callers get
/// the effective options object along with diagnostics for parts of the file
/// graph that were ignored, malformed, unsupported, or inconsistent.
final class AnalysisOptionsParseResult {
/// The initial options file passed to [AnalysisOptionsParseSession.parse].
final File file;
/// The content read from [file], or `null` if it could not be read.
final AnalysisOptionsFileContent? content;
final AnalysisOptionsImpl analysisOptions;
final List<Diagnostic> diagnostics;
AnalysisOptionsParseResult({
required this.file,
required this.content,
required this.analysisOptions,
required this.diagnostics,
});
}
/// Parses analysis options and owns caches reused by parse requests.
final class AnalysisOptionsParseSession {
/// File contents are independent of [SourceFactory].
final Map<File, _FileContent> _fileContents = {};
/// Parsed file nodes with resolved includes.
///
/// Include resolution depends on [SourceFactory], so the same [File] can have
/// more than one file node in this cache.
final Map<({SourceFactory sourceFactory, File file}), _FileNode> _fileNodes =
{};
/// Parses [file] and reports diagnostics for the same file graph.
///
/// This is the production boundary for callers that need the effective
/// [AnalysisOptionsImpl] and the diagnostics produced while interpreting the
/// same initial options file.
AnalysisOptionsParseResult parse({
required SourceFactory sourceFactory,
required Folder contextRoot,
required File file,
VersionConstraint? sdkVersionConstraint,
}) {
return _ParseRequest(
session: this,
sourceFactory: sourceFactory,
contextRoot: contextRoot,
sdkVersionConstraint: sdkVersionConstraint,
).parse(file);
}
_FileContent _fileContentFor(File file) {
if (_fileContents[file] case var result?) {
return result;
}
String text;
try {
text = file.readAsStringSync();
} on FileSystemException catch (exception) {
return _fileContents[file] = _UnreadableFileContent(
file: file,
exception: exception,
);
}
var lineInfo = LineInfo.fromContent(text);
YamlNode yaml;
try {
yaml = loadYamlNode(text);
} on YamlException catch (e) {
return _fileContents[file] = _MalformedYamlFileContent(
file: file,
content: AnalysisOptionsFileContent(
text: text,
lineInfo: lineInfo,
yaml: null,
),
failure: _FileParseFailure(message: e.message, span: e.span),
);
}
return _fileContents[file] = _ParsedYamlFileContent(
file: file,
content: AnalysisOptionsFileContent(
text: text,
lineInfo: lineInfo,
yaml: yaml,
),
);
}
}
/// Mutable state used while applying the parsed include graph to options.
final class _ApplyState {
final AnalysisOptionsBuilder builder;
final Map<String, RuleConfig> _linterRuleConfigs = {};
bool _hasLinterSection = false;
YamlNode? _legacyPluginsNode;
_ApplyState(File file) : builder = AnalysisOptionsBuilder(file: file);
void apply(_ParsedFileData fileData) {
_applyAnalyzer(fileData.analyzer);
_applyCodeStyle(fileData.codeStyle);
_applyFormatter(fileData.formatter);
_applyPlugins(fileData.plugins);
_applyLegacyPlugins(fileData.analyzer.legacyPlugins);
_applyLinterSection(fileData.linter);
}
AnalysisOptionsImpl build() {
if (_hasLinterSection) {
var lintRules = Registry.ruleRegistry.enabled(_linterRuleConfigs);
if (lintRules.isNotEmpty) {
builder.lint = true;
builder.lintRules = lintRules.toList();
}
}
return builder.build();
}
void _applyAnalyzer(_ParsedAnalyzerData analyzer) {
_applyEnableExperiments(analyzer.enableExperiments);
_applyErrorProcessors(analyzer.errorProcessors);
_applyCannotIgnore(analyzer.cannotIgnore);
_applyExcludes(analyzer.excludes);
_applyLanguage(analyzer.language);
_applyOptionalChecks(analyzer.optionalChecks);
}
void _applyCannotIgnore(_ParsedCannotIgnoreData cannotIgnore) {
if (cannotIgnore.names case var names?) {
for (var name in names) {
if (severityMap[name] case var severity?) {
for (var diagnostic in diagnosticCodeValues) {
// If the severity of [error] is also changed in this options file,
// use the changed severity.
var processors = builder.errorProcessors.where(
(processor) => processor.code == diagnostic.lowerCaseName,
);
DiagnosticSeverity? diagnosticSeverity = processors.isNotEmpty
? processors.first.severity
: diagnostic.severity;
if (diagnosticSeverity == severity) {
builder.unignorableDiagnosticCodeNames.add(
diagnostic.lowerCaseName,
);
}
}
} else {
builder.unignorableDiagnosticCodeNames.add(name.toLowerCase());
}
}
}
}
void _applyCodeStyle(_ParsedCodeStyleData codeStyle) {
if (codeStyle.useFormatter case var useFormatter?) {
builder.codeStyleOptions = CodeStyleOptionsImpl(
useFormatter: useFormatter,
);
}
}
void _applyEnableExperiments(_ParsedEnableExperimentsData enableExperiments) {
if (enableExperiments.flags case var flags?) {
builder.contextFeatures =
FeatureSet.fromEnableFlags2(
sdkLanguageVersion: ExperimentStatus.currentVersion,
flags: flags,
)
as ExperimentStatus;
builder.nonPackageFeatureSet = builder.contextFeatures;
}
}
void _applyErrorProcessors(_ParsedErrorProcessorsData errorProcessors) {
if (errorProcessors.processors case var processors?) {
var processorsByCode = {
for (var processor in builder.errorProcessors)
processor.code: processor,
};
for (var processor in processors) {
processorsByCode[processor.code] = processor;
}
builder.errorProcessors = processorsByCode.values.toList();
}
}
void _applyExcludes(_ParsedExcludesData excludes) {
if (excludes.patterns case var patterns?) {
for (var pattern in patterns) {
if (!builder.excludePatterns.contains(pattern)) {
builder.excludePatterns.add(pattern);
}
}
}
}
void _applyFormatter(_ParsedFormatterData formatter) {
builder.formatterOptions = FormatterOptions(
pageWidth: formatter.pageWidth ?? builder.formatterOptions.pageWidth,
trailingCommas:
formatter.trailingCommas ?? builder.formatterOptions.trailingCommas,
);
}
void _applyLanguage(_ParsedLanguageData language) {
if (language.strictCasts case var value?) {
builder.strictCasts = value;
}
if (language.strictInference case var value?) {
builder.strictInference = value;
}
if (language.strictRawTypes case var value?) {
builder.strictRawTypes = value;
}
}
void _applyLegacyPlugins(_ParsedLegacyPluginsData plugins) {
var localNode = plugins.node;
if (localNode == null) {
return;
}
_legacyPluginsNode = _legacyPluginsNode == null
? localNode
: Merger().merge(_legacyPluginsNode!, localNode);
var pluginName = _ParsedLegacyPluginsData.parse(
_legacyPluginsNode,
).firstPluginName;
builder.enabledLegacyPluginNames = [?pluginName];
}
void _applyLinterSection(_ParsedLinterData linter) {
switch (linter.kind) {
case _ParsedLinterDataKind.absent:
return;
case _ParsedLinterDataKind.invalid:
_hasLinterSection = false;
_linterRuleConfigs.clear();
case _ParsedLinterDataKind.valid:
_hasLinterSection = true;
_linterRuleConfigs.addAll(linter.ruleConfigs);
}
}
void _applyOptionalChecks(_ParsedOptionalChecksData optionalChecks) {
if (optionalChecks.chromeOsManifestChecks case var value?) {
builder.chromeOsManifestChecks = value;
}
if (optionalChecks.propagateLinterExceptions case var value?) {
builder.propagateLinterExceptions = value;
}
}
void _applyPlugins(_ParsedPluginsData plugins) {
if (plugins.clearsExisting) {
builder.pluginsOptions = PluginsOptions(
configurations: [],
dependencyOverrides: null,
);
return;
}
var configurations = {
for (var configuration in builder.pluginsOptions.configurations)
configuration.name: configuration,
};
var dependencyOverrides = builder.pluginsOptions.dependencyOverrides == null
? null
: Map.of(builder.pluginsOptions.dependencyOverrides!);
for (var configuration in plugins.configurations) {
configurations[configuration.name] = configuration;
}
if (plugins.dependencyOverrides case var overrides?) {
(dependencyOverrides ??= {}).addAll(overrides);
}
builder.pluginsOptions = PluginsOptions(
configurations: configurations.values.toList(),
dependencyOverrides: dependencyOverrides,
);
}
}
/// Request-specific state and operations for one call to [parse].
final class _ParseRequest {
final AnalysisOptionsParseSession session;
final SourceFactory sourceFactory;
final Folder contextRoot;
final VersionConstraint? sdkVersionConstraint;
final Map<_ParsedFileNode, _ParsedFileData> fileDataCache = {};
_ParseRequest({
required this.session,
required this.sourceFactory,
required this.contextRoot,
required this.sdkVersionConstraint,
});
AnalysisOptionsParseResult parse(File file) {
var initialFileNode = _fileNodeFor(file: file);
var diagnostics = const <Diagnostic>[];
switch (initialFileNode) {
case _ParsedFileNode():
diagnostics = _diagnosticsForParsedFileNode(file: initialFileNode);
case _MalformedYamlFileNode(:var failure):
diagnostics = _diagnosticsForMalformedFile(
file: file,
failure: failure,
);
case _UnreadableFileNode():
break;
}
AnalysisOptionsImpl analysisOptions;
if (initialFileNode case _ParsedFileNode initialParsedFileNode) {
var applyState = _ApplyState(initialParsedFileNode.file);
_applyParsedFiles(
applyState: applyState,
file: initialParsedFileNode,
isInitialFile: true,
includeChain: {initialParsedFileNode.file},
);
analysisOptions = applyState.build();
} else {
analysisOptions = AnalysisOptionsImpl(file: file);
}
return AnalysisOptionsParseResult(
file: file,
content: initialFileNode.content,
analysisOptions: analysisOptions,
diagnostics: diagnostics,
);
}
void _applyParsedFiles({
required _ApplyState applyState,
required _ParsedFileNode file,
required bool isInitialFile,
required Set<File> includeChain,
}) {
for (var include in file.includeResolutions) {
switch (include) {
case _MissingInclude() || _MalformedInclude():
break;
case _ParsedInclude(file: var includedFile):
if (!includeChain.contains(includedFile.file)) {
_applyParsedFiles(
applyState: applyState,
file: includedFile,
isInitialFile: false,
includeChain: {...includeChain, includedFile.file},
);
}
}
}
var data = _parsedFileDataFor(file: file, isInitialFile: isInitialFile);
applyState.apply(data);
}
List<Diagnostic> _diagnosticsForMalformedFile({
required File file,
required _FileParseFailure failure,
}) {
var diagnosticListener = RecordingDiagnosticListener();
var diagnosticReporter = DiagnosticReporter(
diagnosticListener,
FileSource(file),
);
if (failure.span case var span?) {
diagnosticReporter.report(
diag.parseError
.withArguments(errorMessage: failure.message)
.atSourceSpan(span),
);
}
return diagnosticListener.diagnostics;
}
List<Diagnostic> _diagnosticsForParsedFileNode({
required _ParsedFileNode file,
}) {
var diagnosticListener = RecordingDiagnosticListener();
var diagnosticReporter = DiagnosticReporter(
diagnosticListener,
FileSource(file.file),
);
_ParsedFileSemantics computeFileSemantics(
_ParsedFileNode fileNode, {
required bool isInitialFile,
required Set<File> includeChainFiles,
}) {
return _fileSemanticsFor(
file: fileNode,
isInitialFile: isInitialFile,
includeChainFiles: includeChainFiles,
);
}
return _AnalysisOptionsIncludeWalker(
initialDiagnosticListener: diagnosticListener,
initialDiagnosticReporter: diagnosticReporter,
initialParsedFileNode: file,
contextRoot: contextRoot,
fileSemantics: computeFileSemantics,
).walk();
}
_FileNode _fileNodeFor({required File file}) {
var key = (sourceFactory: sourceFactory, file: file);
if (session._fileNodes[key] case var result?) {
return result;
}
var content = session._fileContentFor(file);
switch (content) {
case _ParsedYamlFileContent():
var result = _ParsedFileNode(file: file, content: content.content);
session._fileNodes[key] = result;
result.includeResolutions = _resolvedIncludesFor(
parsedFileNode: result,
);
return result;
case _MalformedYamlFileContent():
return session._fileNodes[key] = _MalformedYamlFileNode(
file: file,
content: content.content,
failure: content.failure,
);
case _UnreadableFileContent():
return session._fileNodes[key] = _UnreadableFileNode(
file: file,
exception: content.exception,
);
}
}
_ParsedFileSemantics _fileSemanticsFor({
required _ParsedFileNode file,
required bool isInitialFile,
required Set<File> includeChainFiles,
}) {
var initialFile = file.file;
_ParsedFileSemantics compute({
required _ParsedFileNode file,
required bool isInitialFile,
required Set<File> includeChainFiles,
}) {
var localData = _parsedFileDataFor(
file: file,
isInitialFile: isInitialFile,
);
var nextIncludeChainFiles = {...includeChainFiles, file.file};
var includedLinterRules = <_IncludedLinterRules>[];
YamlNode? includedLegacyPluginsNode;
for (var include in file.includeResolutions) {
switch (include) {
case _MissingInclude() || _MalformedInclude():
break;
case _ParsedInclude(file: var includedParsedFile):
var includedFile = includedParsedFile.file;
if (includedFile == initialFile ||
includeChainFiles.contains(includedFile)) {
break;
}
var includedSemantics = compute(
file: includedParsedFile,
isInitialFile: false,
includeChainFiles: nextIncludeChainFiles,
);
includedLinterRules.add(
_IncludedLinterRules(
includeNode: include.include.node,
rules: includedSemantics.effectiveLinterRules,
),
);
var includedNode = includedSemantics.effectiveLegacyPluginsNode;
if (includedNode != null) {
includedLegacyPluginsNode = includedLegacyPluginsNode == null
? includedNode
: Merger().merge(includedLegacyPluginsNode, includedNode);
}
}
}
var includedEffectiveLinterRules = _mergeIncludedLinterRules(
includedLinterRules,
);
var effectiveLinterRules = includedEffectiveLinterRules.copy()
..applyLocal(localData.localLinterRules);
return _ParsedFileSemantics(
localData: localData,
includedLinterRules: includedLinterRules,
includedEffectiveLinterRules: includedEffectiveLinterRules,
effectiveLinterRules: effectiveLinterRules,
includedLegacyPluginsNode: includedLegacyPluginsNode,
);
}
return compute(
file: file,
isInitialFile: isInitialFile,
includeChainFiles: includeChainFiles,
);
}
_EffectiveLinterRules _mergeIncludedLinterRules(
List<_IncludedLinterRules> includedRules,
) {
var result = _EffectiveLinterRules();
for (var included in includedRules) {
result.apply(included.rules);
}
return result;
}
_ParsedFileData _parsedFileDataFor({
required _ParsedFileNode file,
required bool isInitialFile,
}) {
return fileDataCache.putIfAbsent(
file,
() => _ParsedFileData.parse(
file,
contextRoot: contextRoot,
sdkVersionConstraint: sdkVersionConstraint,
isInitialFile: isInitialFile,
),
);
}
List<_IncludeResolution> _resolvedIncludesFor({
required _ParsedFileNode parsedFileNode,
}) {
List<_IncludeDirective> includeNodes(YamlMap? yamlMap) {
var includeNode = yamlMap?.valueAt(AnalysisOptionsFileKeys.include);
return switch (includeNode) {
YamlScalar(value: String uri) => [
_IncludeDirective(node: includeNode, uri: uri),
],
YamlList(:var nodes) => [
for (var node in nodes.whereType<YamlScalar>())
if (node.value case String uri)
_IncludeDirective(node: node, uri: uri),
],
_ => const <_IncludeDirective>[],
};
}
_IncludeResolution resolveInclude(_IncludeDirective include) {
var includeSource = sourceFactory.resolveUri(
FileSource(parsedFileNode.file),
include.uri,
);
if (includeSource is! FileSource) {
return _MissingInclude(include: include);
}
var includedFile = includeSource.file;
var includedFileNode = _fileNodeFor(file: includedFile);
switch (includedFileNode) {
case _ParsedFileNode():
return _ParsedInclude(include: include, file: includedFileNode);
case _MalformedYamlFileNode():
return _MalformedInclude(include: include, file: includedFileNode);
case _UnreadableFileNode():
return _MissingInclude(include: include);
}
}
var yamlMap = parsedFileNode.content.yaml.tryCast<YamlMap>();
return [for (var include in includeNodes(yamlMap)) resolveInclude(include)];
}
}
extension on YamlNode? {
bool get isNullScalar {
var self = this;
return self is YamlScalar && self.value == null;
}
String? get stringValue {
var self = this;
if (self is YamlScalar) {
var value = self.value;
if (value is String) {
return value;
}
}
return null;
}
}