| // 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.file, 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(File file, _ParsedAnalyzerData analyzer) { | |
| _applyEnableExperiments(analyzer.enableExperiments); | |
| _applyErrorProcessors(analyzer.errorProcessors); | |
| _applyCannotIgnore(analyzer.cannotIgnore); | |
| _applyExcludes(file, 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(File file, _ParsedExcludesData excludes) { | |
| if (excludes.patterns case var patterns?) { | |
| for (var pattern in patterns) { | |
| builder.excludePatterns.add( | |
| AnalysisOptionsExcludePattern(declaringFile: file, pattern: 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, | |
| 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; | |
| } | |
| } |