| // 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', | |
| // TODO(scheglov): Remove this compatibility entry together with | |
| // `test_analyzer_errors_removed_pluginsInInnerOptions` after Flutter | |
| // removes its `plugins_in_inner_options` suppression. | |
| 'PLUGINS_IN_INNER_OPTIONS', | |
| // Annotations report undefined names like other expressions, as | |
| // `UNDEFINED_UNQUALIFIED_NAME_READ` or `UNDEFINED_PREFIXED_NAME_READ`. | |
| 'UNDEFINED_ANNOTATION', | |
| // Split into `UNDEFINED_SUPER_MEMBER_READ`, `UNDEFINED_SUPER_MEMBER_WRITE`, | |
| // and `UNDEFINED_SUPER_OPERATOR`. | |
| 'UNDEFINED_SUPER_MEMBER', | |
| // Static members report `UNDEFINED_STATIC_MEMBER_READ` and | |
| // `UNDEFINED_STATIC_MEMBER_WRITE`. | |
| 'PRIVATE_SETTER', | |
| 'UNDEFINED_ENUM_CONSTANT', | |
| 'UNDEFINED_EXTENSION_GETTER', | |
| 'UNDEFINED_EXTENSION_METHOD', | |
| 'UNDEFINED_EXTENSION_SETTER', | |
| // Dot shorthands report `UNDEFINED_STATIC_MEMBER_READ`. | |
| 'DOT_SHORTHAND_UNDEFINED_MEMBER', | |
| // Instance members report `UNDEFINED_INSTANCE_MEMBER_READ`. | |
| 'UNDEFINED_GETTER', | |
| // Instance members report `UNDEFINED_INSTANCE_MEMBER_WRITE`. | |
| 'UNDEFINED_SETTER', | |
| // Split into `UNDEFINED_PREFIXED_NAME_READ` and | |
| // `UNDEFINED_PREFIXED_NAME_WRITE`. | |
| 'UNDEFINED_PREFIXED_NAME', | |
| // Unqualified names report `UNDEFINED_UNQUALIFIED_NAME_READ` and | |
| // `UNDEFINED_UNQUALIFIED_NAME_WRITE`. | |
| 'ASSIGNMENT_TO_CONST', | |
| 'ASSIGNMENT_TO_FINAL', | |
| 'ASSIGNMENT_TO_FINAL_NO_SETTER', | |
| 'ASSIGNMENT_TO_FUNCTION', | |
| 'ASSIGNMENT_TO_METHOD', | |
| 'ASSIGNMENT_TO_TYPE', | |
| 'UNDEFINED_FUNCTION', | |
| 'UNDEFINED_IDENTIFIER', | |
| 'UNDEFINED_METHOD', | |
| }; | |
| 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); | |
| if (pattern.startsWith('/')) { | |
| reporter.report( | |
| diag.excludeInvalidGlob | |
| .withArguments(pattern: pattern) | |
| .atSourceSpan(node.span), | |
| ); | |
| } | |
| } 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 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, | |
| 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 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); | |
| } | |
| 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; | |
| } |