| // Copyright (c) 2015, 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/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/source.dart'; |
| import 'package:analyzer/src/analysis_options/analysis_options_file.dart'; |
| import 'package:analyzer/src/analysis_options/analysis_options_provider.dart'; |
| import 'package:analyzer/src/analysis_options/options_validator.dart'; |
| import 'package:analyzer/src/dart/analysis/experiments.dart'; |
| import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; |
| import 'package:analyzer/src/error/listener.dart'; |
| import 'package:analyzer/src/generated/source.dart' show SourceFactory; |
| import 'package:analyzer/src/lint/options_rule_validator.dart'; |
| import 'package:analyzer/src/lint/registry.dart'; |
| import 'package:analyzer/src/util/yaml.dart'; |
| import 'package:analyzer/src/utilities/extensions/source.dart'; |
| import 'package:analyzer/src/utilities/extensions/string.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:source_span/source_span.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| /// Returns the name of the first legacy plugin, if one is specified in |
| /// [options], otherwise `null`. |
| String? _firstPluginName(YamlMap options) { |
| var analyzerMap = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzerMap is! YamlMap) { |
| return null; |
| } |
| var plugins = analyzerMap.valueAt(AnalysisOptionsFileKeys.plugins); |
| if (plugins is YamlScalar) { |
| return plugins.value as String?; |
| } else if (plugins is YamlList) { |
| return plugins.first as String?; |
| } else if (plugins is YamlMap) { |
| return plugins.keys.first as String?; |
| } else { |
| return null; |
| } |
| } |
| |
| /// Validates the legacy 'plugins' options in [options], given |
| /// [firstEnabledPluginName]. |
| void _validateLegacyPluginsOption( |
| DiagnosticReporter diagnosticReporter, { |
| required YamlMap options, |
| String? firstEnabledPluginName, |
| }) { |
| _LegacyPluginsOptionValidator( |
| firstEnabledPluginName, |
| ).validate(diagnosticReporter, options); |
| } |
| |
| class AnalysisOptionsAnalyzer { |
| RecordingDiagnosticListener initialDiagnosticListener = |
| RecordingDiagnosticListener(); |
| DiagnosticReporter initialDiagnosticReporter; |
| DiagnosticListener diagnosticListener; |
| DiagnosticReporter diagnosticReporter; |
| final SourceFactory sourceFactory; |
| final String contextRoot; |
| final VersionConstraint? sdkVersionConstraint; |
| final ResourceProvider resourceProvider; |
| SourceSpan? initialIncludeSpan; |
| final AnalysisOptionsProvider optionsProvider; |
| String? firstPluginName; |
| |
| /// Map whose keys are source files containing an `include` directive that is |
| /// currently being visited, and whose values are the corresponding |
| /// [SourceSpan]s of those `include` directives. |
| final Map<Source, SourceSpan> includeChain = {}; |
| |
| final AnalysisOptionsCache _analysisOptionsCache; |
| |
| factory AnalysisOptionsAnalyzer({ |
| required Source initialSource, |
| required SourceFactory sourceFactory, |
| required String contextRoot, |
| required VersionConstraint? sdkVersionConstraint, |
| required ResourceProvider resourceProvider, |
| AnalysisOptionsCache? analysisOptionsCache, |
| }) { |
| var initialDiagnosticListener = RecordingDiagnosticListener(); |
| var initialDiagnosticReporter = DiagnosticReporter( |
| initialDiagnosticListener, |
| initialSource, |
| ); |
| return AnalysisOptionsAnalyzer._( |
| initialDiagnosticListener: initialDiagnosticListener, |
| initialDiagnosticReporter: initialDiagnosticReporter, |
| diagnosticListener: initialDiagnosticListener, |
| diagnosticReporter: initialDiagnosticReporter, |
| sourceFactory: sourceFactory, |
| contextRoot: contextRoot, |
| sdkVersionConstraint: sdkVersionConstraint, |
| resourceProvider: resourceProvider, |
| optionsProvider: AnalysisOptionsProvider(sourceFactory), |
| analysisOptionsCache: analysisOptionsCache ?? {}, |
| ); |
| } |
| |
| AnalysisOptionsAnalyzer._({ |
| required this.initialDiagnosticListener, |
| required this.initialDiagnosticReporter, |
| required this.diagnosticListener, |
| required this.diagnosticReporter, |
| required this.sourceFactory, |
| required this.contextRoot, |
| required this.sdkVersionConstraint, |
| required this.resourceProvider, |
| required this.optionsProvider, |
| required AnalysisOptionsCache analysisOptionsCache, |
| }) : _analysisOptionsCache = analysisOptionsCache; |
| |
| Source get initialSource => initialDiagnosticReporter.source; |
| |
| /// Whether the source file currently being visited is the initial source |
| /// file. |
| bool get isPrimarySource => source == initialSource; |
| |
| Source get source => diagnosticReporter.source; |
| |
| List<Diagnostic> walkIncludes({required String content}) { |
| try { |
| YamlMap options = optionsProvider.getOptionsFromString( |
| content, |
| sourceUrl: initialSource.uri, |
| ); |
| _validate(options); |
| } on OptionsFormatException catch (e) { |
| SourceSpan span = e.span!; |
| diagnosticReporter.report( |
| diag.parseError |
| .withArguments(errorMessage: e.message) |
| .atSourceSpan(span), |
| ); |
| } |
| // Make sure `initialIncludeSpan`, `source`, `includeChain`, |
| // `diagnosticListener`, and `diagnosticReporter` have been restored to |
| // their original states. |
| assert(initialIncludeSpan == null); |
| assert(identical(source, initialSource)); |
| assert(includeChain.isEmpty); |
| assert(identical(diagnosticListener, initialDiagnosticListener)); |
| assert(identical(diagnosticReporter, initialDiagnosticReporter)); |
| return initialDiagnosticListener.diagnostics; |
| } |
| |
| // Validates the specified options and any included option files. |
| void _validate(YamlMap options) { |
| OptionsFileValidator( |
| source, |
| sdkVersionConstraint: sdkVersionConstraint, |
| contextRoot: contextRoot, |
| isPrimarySource: isPrimarySource, |
| optionsProvider: optionsProvider, |
| sourceFactory: sourceFactory, |
| resourceProvider: resourceProvider, |
| analysisOptionsCache: _analysisOptionsCache, |
| ).validate(options, diagnosticReporter); |
| |
| var includeNode = options.valueAt(AnalysisOptionsFileKeys.include); |
| if (includeNode == null) { |
| // Validate the 'plugins' option in [options], understanding that no other |
| // options are included. |
| _validateLegacyPluginsOption(diagnosticReporter, options: options); |
| return; |
| } |
| |
| var includes = switch (includeNode) { |
| YamlScalar node => [node], |
| YamlList(:var nodes) => nodes.whereType<YamlScalar>().toList(), |
| _ => const <YamlScalar>[], |
| }; |
| |
| for (var includeValue in includes) { |
| var previousInitialIncludeSpan = initialIncludeSpan; |
| try { |
| var includeSpan = includeValue.span; |
| initialIncludeSpan ??= includeSpan; |
| _validateInclude(options, includeSpan); |
| } finally { |
| initialIncludeSpan = previousInitialIncludeSpan; |
| } |
| } |
| } |
| |
| void _validateInclude(YamlMap options, SourceSpan includeSpan) { |
| assert(initialIncludeSpan != null); |
| var includeUri = includeSpan.text; |
| var (first, last) = ( |
| includeUri.codeUnits.firstOrNull, |
| includeUri.codeUnits.lastOrNull, |
| ); |
| if ((first == 0x0022 || first == 0x0027) && first == last) { |
| // The URI begins and ends with either a double quote or single quote |
| // i.e. the value of the "include" field is quoted. |
| includeUri = includeUri.substring(1, includeUri.length - 1); |
| } |
| |
| var includedSource = sourceFactory.resolveUri(source, includeUri); |
| if (includedSource == initialSource) { |
| initialDiagnosticReporter.report( |
| diag.recursiveIncludeFile |
| .withArguments( |
| includedUri: includeUri, |
| includingFilePath: source.fullName, |
| ) |
| .atSourceSpan(initialIncludeSpan!), |
| ); |
| return; |
| } |
| if (includedSource == null || !includedSource.exists()) { |
| initialDiagnosticReporter.report( |
| diag.includeFileNotFound |
| .withArguments( |
| includedUri: includeUri, |
| includingFilePath: source.fullName, |
| contextRootPath: contextRoot, |
| ) |
| .atSourceSpan(initialIncludeSpan!), |
| ); |
| return; |
| } |
| |
| try { |
| var includedOptions = optionsProvider.getOptionsFromString( |
| includedSource.stringContents, |
| ); |
| var previousDiagnosticListener = diagnosticListener; |
| var previousDiagnosticReporter = diagnosticReporter; |
| try { |
| assert(includeChain[source] == null); |
| includeChain[source] = includeSpan; |
| var spanInChain = includeChain[includedSource]; |
| if (spanInChain != null) { |
| initialDiagnosticReporter.report( |
| diag.includedFileWarning |
| .withArguments( |
| includingFilePath: includedSource.fullName, |
| startOffset: spanInChain.start.offset, |
| endOffset: spanInChain.end.offset - 1, |
| warningMessage: 'The file includes itself recursively.', |
| ) |
| .atSourceSpan(initialIncludeSpan!), |
| ); |
| return; |
| } |
| diagnosticListener = _IncludedDiagnosticListener( |
| source: includedSource, |
| initialDiagnosticReporter: initialDiagnosticReporter, |
| initialIncludeSpan: initialIncludeSpan!, |
| ); |
| diagnosticReporter = DiagnosticReporter( |
| diagnosticListener, |
| includedSource, |
| ); |
| _validate(includedOptions); |
| } finally { |
| diagnosticListener = previousDiagnosticListener; |
| diagnosticReporter = previousDiagnosticReporter; |
| includeChain.remove(source); |
| } |
| firstPluginName ??= _firstPluginName(includedOptions); |
| // Validate the 'plugins' option in [options], taking into account any |
| // plugins enabled by [includedOptions]. |
| _validateLegacyPluginsOption( |
| diagnosticReporter, |
| options: options, |
| firstEnabledPluginName: firstPluginName, |
| ); |
| } on OptionsFormatException catch (e) { |
| // Report diagnostics for included option files on the `include` directive |
| // located in the initial options file. |
| initialDiagnosticReporter.report( |
| diag.includedFileParseError |
| .withArguments( |
| includingFilePath: includedSource.fullName, |
| startOffset: e.span!.start.offset, |
| endOffset: e.span!.end.offset, |
| errorMessage: e.message, |
| ) |
| .atSourceSpan(initialIncludeSpan!), |
| ); |
| } |
| } |
| } |
| |
| /// Validates `analyzer` options. |
| class AnalyzerOptionsValidator extends _CompositeValidator { |
| AnalyzerOptionsValidator() |
| : super([ |
| _AnalyzerTopLevelOptionsValidator(), |
| _StrongModeOptionValueValidator(), |
| _ErrorFilterOptionValidator(), |
| _EnableExperimentsValidator(), |
| _LanguageOptionValidator(), |
| _OptionalChecksValueValidator(), |
| _CannotIgnoreOptionValidator(), |
| ]); |
| } |
| |
| /// Validates options defined in an analysis options file. |
| @visibleForTesting |
| class OptionsFileValidator { |
| final List<OptionsValidator> _validators; |
| |
| OptionsFileValidator( |
| Source source, { |
| VersionConstraint? sdkVersionConstraint, |
| required String contextRoot, |
| required bool isPrimarySource, |
| required AnalysisOptionsProvider optionsProvider, |
| required ResourceProvider resourceProvider, |
| required SourceFactory sourceFactory, |
| required AnalysisOptionsCache analysisOptionsCache, |
| }) : _validators = [ |
| AnalyzerOptionsValidator(), |
| _CodeStyleOptionsValidator(), |
| _FormatterOptionsValidator(), |
| _LinterTopLevelOptionsValidator(), |
| LinterRuleOptionsValidator( |
| resourceProvider: resourceProvider, |
| optionsProvider: optionsProvider, |
| sourceFactory: sourceFactory, |
| sdkVersionConstraint: sdkVersionConstraint, |
| isPrimarySource: isPrimarySource, |
| analysisOptionsCache: analysisOptionsCache, |
| ), |
| _PluginsOptionsValidator( |
| contextRoot: contextRoot, |
| filePath: source.fullName, |
| isPrimarySource: isPrimarySource, |
| resourceProvider: resourceProvider, |
| ), |
| ]; |
| |
| void validate(YamlMap options, DiagnosticReporter reporter) { |
| for (var validator in _validators) { |
| validator.validate(reporter, options); |
| } |
| } |
| } |
| |
| /// Validates `analyzer` top-level options. |
| class _AnalyzerTopLevelOptionsValidator extends _TopLevelOptionValidator { |
| _AnalyzerTopLevelOptionsValidator() |
| : super( |
| AnalysisOptionsFileKeys.analyzer, |
| AnalysisOptionsFileKeys.analyzerOptions, |
| ); |
| } |
| |
| /// Validates the `analyzer` `cannot-ignore` option. |
| /// |
| /// This includes the format of the `cannot-ignore` section, the format of |
| /// values in the section, and whether each value is a valid string. |
| class _CannotIgnoreOptionValidator extends OptionsValidator { |
| /// Lazily populated set of diagnostic code names. |
| static final Set<String> _diagnosticCodes = diagnosticCodeValues |
| .map((DiagnosticCode code) => code.lowerCaseName.toUpperCase()) |
| .toSet(); |
| |
| /// 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> _removedDiagnosticCodes = {'MISSING_RETURN'}; |
| |
| /// Lazily populated set of lint code names. |
| late final Set<String> _lintCodes = Registry.ruleRegistry.rules |
| .map((rule) => rule.name.toUpperCase()) |
| .toSet(); |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var analyzer = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzer is YamlMap) { |
| var unignorableNames = analyzer.valueAt( |
| AnalysisOptionsFileKeys.cannotIgnore, |
| ); |
| if (unignorableNames is YamlList) { |
| var listedNames = <String>{}; |
| for (var unignorableNameNode in unignorableNames.nodes) { |
| var unignorableName = unignorableNameNode.value; |
| if (unignorableName is String) { |
| if (AnalysisOptionsFileKeys.severities.contains(unignorableName)) { |
| listedNames.add(unignorableName); |
| continue; |
| } |
| var upperCaseName = unignorableName.toUpperCase(); |
| if (!_diagnosticCodes.contains(upperCaseName) && |
| !_lintCodes.contains(upperCaseName) && |
| !_removedDiagnosticCodes.contains(upperCaseName)) { |
| reporter.report( |
| diag.unrecognizedErrorCode |
| .withArguments(codeName: unignorableName) |
| .atSourceSpan(unignorableNameNode.span), |
| ); |
| } else if (listedNames.contains(upperCaseName)) { |
| // TODO(srawlins): Create a "duplicate value" code and report it |
| // here. |
| } else { |
| listedNames.add(upperCaseName); |
| } |
| } else { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.cannotIgnore, |
| ) |
| .atSourceSpan(unignorableNameNode.span), |
| ); |
| } |
| } |
| } else if (unignorableNames != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.cannotIgnore) |
| .atSourceSpan(unignorableNames.span), |
| ); |
| } |
| } |
| } |
| } |
| |
| /// Validates `code-style` options. |
| class _CodeStyleOptionsValidator extends OptionsValidator { |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var codeStyle = options.valueAt(AnalysisOptionsFileKeys.codeStyle); |
| if (codeStyle is YamlMap) { |
| codeStyle.nodeMap.forEach((keyNode, valueNode) { |
| var key = keyNode.value; |
| if (key == AnalysisOptionsFileKeys.format) { |
| _validateFormat(reporter, valueNode); |
| } else { |
| reporter.report( |
| diag.unsupportedOptionWithoutValues |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.codeStyle, |
| optionKey: keyNode.toString(), |
| ) |
| .atSourceSpan(keyNode.span), |
| ); |
| } |
| }); |
| } else if (codeStyle is YamlScalar && codeStyle.value != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.codeStyle) |
| .atSourceSpan(codeStyle.span), |
| ); |
| } else if (codeStyle is YamlList) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.codeStyle) |
| .atSourceSpan(codeStyle.span), |
| ); |
| } |
| } |
| |
| void _validateFormat(DiagnosticReporter reporter, YamlNode format) { |
| if (format is! YamlScalar) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.format) |
| .atSourceSpan(format.span), |
| ); |
| return; |
| } |
| var formatValue = format.toBool(); |
| if (formatValue == null) { |
| reporter.report( |
| diag.unsupportedValue |
| .withArguments( |
| optionName: AnalysisOptionsFileKeys.format, |
| invalidValue: format.value.toString(), |
| legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal, |
| ) |
| .atSourceSpan(format.span), |
| ); |
| } |
| } |
| } |
| |
| /// Convenience class for composing validators. |
| class _CompositeValidator extends OptionsValidator { |
| final List<OptionsValidator> validators; |
| |
| _CompositeValidator(this.validators); |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| for (var validator in validators) { |
| validator.validate(reporter, options); |
| } |
| } |
| } |
| |
| /// Validates the `analyzer` `enable-experiments` configuration options. |
| class _EnableExperimentsValidator extends OptionsValidator { |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var analyzer = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzer is YamlMap) { |
| var experimentNames = analyzer.valueAt( |
| AnalysisOptionsFileKeys.enableExperiment, |
| ); |
| if (experimentNames is YamlList) { |
| var flags = experimentNames.nodes |
| .map((node) => node.toString()) |
| .toList(); |
| for (var validationResult in validateFlags(flags)) { |
| var flagIndex = validationResult.stringIndex; |
| var span = experimentNames.nodes[flagIndex].span; |
| 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), |
| ); |
| } |
| } |
| } else if (experimentNames != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.enableExperiment, |
| ) |
| .atSourceSpan(experimentNames.span), |
| ); |
| } |
| } |
| } |
| } |
| |
| /// 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}); |
| } |
| |
| /// Validates `analyzer` error filter options. |
| class _ErrorFilterOptionValidator extends OptionsValidator { |
| /// Legal values. |
| static final List<String> legalValues = [ |
| ...AnalysisOptionsFileKeys.ignoreSynonyms, |
| ...AnalysisOptionsFileKeys.trueOrFalse, |
| ...AnalysisOptionsFileKeys.severities, |
| ]; |
| |
| /// Pretty String listing legal values. |
| static final String legalValueString = |
| legalValues.quotedAndCommaSeparatedWithAnd; |
| |
| /// Lazily populated set of diagnostic code names. |
| static final Set<String> _diagnosticCodes = diagnosticCodeValues |
| .map((DiagnosticCode code) => code.lowerCaseName.toUpperCase()) |
| .toSet(); |
| |
| /// 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> _removedDiagnosticCodes = {'MISSING_RETURN'}; |
| |
| /// Lazily populated set of lint code names. |
| late final Set<String> _lintCodes = Registry.ruleRegistry.rules |
| .map((rule) => rule.name.toUpperCase()) |
| .toSet(); |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var analyzer = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzer is YamlMap) { |
| var filters = analyzer.valueAt(AnalysisOptionsFileKeys.errors); |
| if (filters is YamlMap) { |
| filters.nodes.forEach((k, v) { |
| String? value; |
| if (k is YamlScalar) { |
| value = k.toUpperCase(); |
| if (!_diagnosticCodes.contains(value) && |
| !_lintCodes.contains(value) && |
| !_removedDiagnosticCodes.contains(value)) { |
| reporter.report( |
| diag.unrecognizedErrorCode |
| .withArguments(codeName: k.value.toString()) |
| .atSourceSpan(k.span), |
| ); |
| } |
| } |
| if (v is YamlScalar) { |
| value = v.toLowerCase(); |
| if (!legalValues.contains(value)) { |
| reporter.report( |
| diag.unsupportedOptionWithLegalValues |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.errors, |
| optionKey: v.value.toString(), |
| legalValues: legalValueString, |
| ) |
| .atSourceSpan(v.span), |
| ); |
| } |
| } else { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.enableExperiment, |
| ) |
| .atSourceSpan(v.span), |
| ); |
| } |
| }); |
| } else if (filters != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.enableExperiment, |
| ) |
| .atSourceSpan(filters.span), |
| ); |
| } |
| } |
| } |
| } |
| |
| /// Validates `formatter` options. |
| class _FormatterOptionsValidator extends OptionsValidator { |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var formatter = options.valueAt(AnalysisOptionsFileKeys.formatter); |
| if (formatter == null) { |
| return; |
| } |
| |
| if (formatter is YamlMap) { |
| for (var MapEntry(key: keyNode, value: valueNode) |
| in formatter.nodeMap.entries) { |
| if (keyNode.value == AnalysisOptionsFileKeys.pageWidth) { |
| _validatePageWidth(keyNode, valueNode, reporter); |
| } else if (keyNode.value == AnalysisOptionsFileKeys.trailingCommas) { |
| _validateTrailingCommas(keyNode, valueNode, reporter); |
| } else { |
| reporter.report( |
| diag.unsupportedOptionWithoutValues |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.formatter, |
| optionKey: keyNode.toString(), |
| ) |
| .atSourceSpan(keyNode.span), |
| ); |
| } |
| } |
| } else if (formatter.value != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.formatter) |
| .atSourceSpan(formatter.span), |
| ); |
| } |
| } |
| |
| void _validatePageWidth( |
| YamlNode keyNode, |
| YamlNode valueNode, |
| DiagnosticReporter reporter, |
| ) { |
| var value = valueNode.value; |
| if (value is! int || value <= 0) { |
| reporter.report( |
| diag.invalidOption |
| .withArguments( |
| optionName: keyNode.toString(), |
| detailMessage: '"page_width" must be a positive integer.', |
| ) |
| .atSourceSpan(valueNode.span), |
| ); |
| } |
| } |
| |
| void _validateTrailingCommas( |
| YamlNode keyNode, |
| YamlNode valueNode, |
| DiagnosticReporter reporter, |
| ) { |
| var value = valueNode.value; |
| |
| if (!TrailingCommas.values.any((item) => item.name == value)) { |
| reporter.report( |
| diag.invalidOption |
| .withArguments( |
| optionName: keyNode.toString(), |
| detailMessage: |
| '"trailing_commas" must be "automate" or "preserve".', |
| ) |
| .atSourceSpan(valueNode.span), |
| ); |
| } |
| } |
| } |
| |
| /// Implementation of [DiagnosticListener] that converts each reported |
| /// [Diagnostic] into a [diag.includedFileWarning] located at the site of an |
| /// `include` directive. |
| /// |
| /// This is used by [AnalysisOptionsAnalyzer] to report diagnostics that occur |
| /// in included options files. |
| class _IncludedDiagnosticListener implements DiagnosticListener { |
| /// The [Source] file in which diagnostics are being reported. |
| final Source source; |
| |
| /// The [DiagnosticReporter] for the initial soure file (the one containing |
| /// the first `include` in the chain of `include`s that's currently being |
| /// processed). |
| final DiagnosticReporter initialDiagnosticReporter; |
| |
| /// The [SourceSpan] of the first `include` in the chain of `include`s that's |
| /// currently being processed. |
| final SourceSpan initialIncludeSpan; |
| |
| _IncludedDiagnosticListener({ |
| required this.source, |
| required this.initialDiagnosticReporter, |
| required this.initialIncludeSpan, |
| }); |
| |
| @override |
| void onDiagnostic(Diagnostic diagnostic) { |
| initialDiagnosticReporter.report( |
| diag.includedFileWarning |
| .withArguments( |
| includingFilePath: source.fullName, |
| startOffset: diagnostic.offset, |
| endOffset: diagnostic.offset + diagnostic.length - 1, |
| warningMessage: diagnostic.message, |
| ) |
| .atSourceSpan(initialIncludeSpan), |
| ); |
| } |
| } |
| |
| /// Validates `analyzer` language configuration options. |
| class _LanguageOptionValidator extends OptionsValidator { |
| final _ErrorBuilder _builder = _ErrorBuilder( |
| AnalysisOptionsFileKeys.languageOptions, |
| ); |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var analyzer = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzer is YamlMap) { |
| var language = analyzer.valueAt(AnalysisOptionsFileKeys.language); |
| if (language is YamlMap) { |
| language.nodes.forEach((k, v) { |
| String? key; |
| bool validKey = false; |
| if (k is YamlScalar) { |
| key = k.value?.toString(); |
| if (!AnalysisOptionsFileKeys.languageOptions.contains(key)) { |
| _builder.reportError( |
| reporter, |
| AnalysisOptionsFileKeys.language, |
| k, |
| ); |
| } else { |
| // If we have a valid key, go on and check the value. |
| validKey = true; |
| } |
| } |
| if (validKey && v is YamlScalar) { |
| if (v.toBool() == null) { |
| // `null` is not a valid key, so we can safely assume `key` is |
| // non-`null`. |
| reporter.report( |
| diag.unsupportedValue |
| .withArguments( |
| optionName: key!, |
| invalidValue: v.value.toString(), |
| legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal, |
| ) |
| .atSourceSpan(v.span), |
| ); |
| } |
| } |
| }); |
| } else if (language is YamlScalar && language.value != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.language) |
| .atSourceSpan(language.span), |
| ); |
| } else if (language is YamlList) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.language) |
| .atSourceSpan(language.span), |
| ); |
| } |
| } |
| } |
| } |
| |
| /// Validates `analyzer` plugins configuration options. |
| class _LegacyPluginsOptionValidator extends OptionsValidator { |
| /// The name of the first included legacy plugin, if there is one. |
| /// |
| /// If there are no included legacy plugins, this is `null`. |
| final String? _firstIncludedPluginName; |
| |
| _LegacyPluginsOptionValidator(this._firstIncludedPluginName); |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var analyzer = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzer is! YamlMap) { |
| return; |
| } |
| var plugins = analyzer.valueAt(AnalysisOptionsFileKeys.plugins); |
| if (plugins is YamlScalar && plugins.value != null) { |
| if (_firstIncludedPluginName != null && |
| _firstIncludedPluginName != plugins.value) { |
| reporter.report( |
| diag.multiplePlugins |
| .withArguments(firstPluginName: _firstIncludedPluginName) |
| .atSourceSpan(plugins.span), |
| ); |
| } |
| } else if (plugins is YamlList) { |
| var pluginValues = plugins.nodes.whereType<YamlNode>().toList(); |
| if (_firstIncludedPluginName != null) { |
| // There is already at least one plugin specified in included options. |
| for (var plugin in pluginValues) { |
| if (plugin.value != _firstIncludedPluginName) { |
| reporter.report( |
| diag.multiplePlugins |
| .withArguments(firstPluginName: _firstIncludedPluginName) |
| .atSourceSpan(plugin.span), |
| ); |
| } |
| } |
| } else if (plugins.length > 1) { |
| String? firstPlugin; |
| for (var plugin in pluginValues) { |
| if (firstPlugin == null) { |
| var pluginValue = plugin.value; |
| if (pluginValue is String) { |
| firstPlugin = pluginValue; |
| continue; |
| } else { |
| // This plugin is bad and should not be marked as the first one. |
| continue; |
| } |
| } else if (plugin.value != firstPlugin) { |
| reporter.report( |
| diag.multiplePlugins |
| .withArguments(firstPluginName: firstPlugin) |
| .atSourceSpan(plugin.span), |
| ); |
| } |
| } |
| } |
| } else if (plugins is YamlMap) { |
| var pluginValues = plugins.nodes.keys.cast<YamlNode?>(); |
| if (_firstIncludedPluginName != null) { |
| // There is already at least one plugin specified in included options. |
| for (var plugin in pluginValues) { |
| if (plugin != null && plugin.value != _firstIncludedPluginName) { |
| reporter.report( |
| diag.multiplePlugins |
| .withArguments(firstPluginName: _firstIncludedPluginName) |
| .atSourceSpan(plugin.span), |
| ); |
| } |
| } |
| } else if (plugins.length > 1) { |
| String? firstPlugin; |
| for (var plugin in pluginValues) { |
| if (firstPlugin == null) { |
| var pluginValue = plugin?.value; |
| if (pluginValue is String) { |
| firstPlugin = pluginValue; |
| continue; |
| } else { |
| // This plugin is bad and should not be marked as the first one. |
| continue; |
| } |
| } else if (plugin != null && plugin.value != firstPlugin) { |
| reporter.report( |
| diag.multiplePlugins |
| .withArguments(firstPluginName: firstPlugin) |
| .atSourceSpan(plugin.span), |
| ); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /// Validates `linter` top-level options. |
| class _LinterTopLevelOptionsValidator extends _TopLevelOptionValidator { |
| _LinterTopLevelOptionsValidator() |
| : super( |
| AnalysisOptionsFileKeys.linter, |
| AnalysisOptionsFileKeys.linterOptions, |
| ); |
| } |
| |
| /// Validates `analyzer` optional-checks value configuration options. |
| class _OptionalChecksValueValidator extends OptionsValidator { |
| final _ErrorBuilder _builder = _ErrorBuilder( |
| AnalysisOptionsFileKeys.optionalChecksOptions, |
| ); |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var analyzer = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzer is YamlMap) { |
| var v = analyzer.valueAt(AnalysisOptionsFileKeys.optionalChecks); |
| if (v is YamlScalar) { |
| var value = v.toLowerCase(); |
| if (!AnalysisOptionsFileKeys.optionalChecksOptions.contains(value)) { |
| _builder.reportError( |
| reporter, |
| AnalysisOptionsFileKeys |
| .optionalChecksOptions |
| .quotedAndCommaSeparatedWithOr, |
| v, |
| ); |
| } |
| } else if (v is YamlMap) { |
| v.nodes.forEach((k, v) { |
| if (k is YamlScalar) { |
| var key = k.value?.toString(); |
| if (!AnalysisOptionsFileKeys.optionalChecksOptions.contains(key)) { |
| _builder.reportError( |
| reporter, |
| AnalysisOptionsFileKeys |
| .optionalChecksOptions |
| .quotedAndCommaSeparatedWithOr, |
| k, |
| ); |
| } else { |
| if (v is! YamlScalar || v.toBool() == null) { |
| reporter.report( |
| diag.unsupportedValue |
| .withArguments( |
| optionName: key!, |
| invalidValue: v.value.toString(), |
| legalValues: |
| AnalysisOptionsFileKeys.trueOrFalseProposal, |
| ) |
| .atSourceSpan(v.span), |
| ); |
| } |
| } |
| } |
| }); |
| } else if (v != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments( |
| sectionName: AnalysisOptionsFileKeys.enableExperiment, |
| ) |
| .atSourceSpan(v.span), |
| ); |
| } |
| } |
| } |
| } |
| |
| /// Validates options for each `plugins` map value. |
| class _PluginsOptionsValidator extends OptionsValidator { |
| final _ErrorBuilder _builder = _ErrorBuilder( |
| AnalysisOptionsFileKeys.pluginsOptions, |
| ); |
| |
| final _ErrorBuilder _gitBuilder = _ErrorBuilder( |
| AnalysisOptionsFileKeys.gitOptions, |
| ); |
| |
| final String _contextRoot; |
| |
| final String _filePath; |
| |
| final bool _isPrimarySource; |
| |
| final ResourceProvider _resourceProvider; |
| |
| _PluginsOptionsValidator({ |
| required String contextRoot, |
| required String filePath, |
| required bool isPrimarySource, |
| required ResourceProvider resourceProvider, |
| }) : _contextRoot = contextRoot, |
| _filePath = filePath, |
| _isPrimarySource = isPrimarySource, |
| _resourceProvider = resourceProvider; |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var plugins = options.valueAt(AnalysisOptionsFileKeys.plugins); |
| switch (plugins) { |
| case YamlMap(): |
| var sourceDir = _resourceProvider.pathContext.dirname(_filePath); |
| var isAtContextRoot = sourceDir == _contextRoot; |
| if (!isAtContextRoot && _isPrimarySource) { |
| reporter.report( |
| diag.pluginsInInnerOptions |
| .withArguments(contextRoot: _contextRoot) |
| .atSourceSpan(plugins.span), |
| ); |
| } |
| plugins.nodes.forEach((pluginNameNode, pluginValue) { |
| if (pluginNameNode is! YamlScalar) { |
| return; |
| } |
| var pluginName = pluginNameNode.value; |
| if (pluginName is! String) { |
| return; |
| } |
| 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, pluginName, pluginValue); |
| default: |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments( |
| sectionName: |
| '${AnalysisOptionsFileKeys.plugins}/$pluginName', |
| ) |
| .atSourceSpan(pluginValue.span), |
| ); |
| } |
| }); |
| case YamlList(): |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.plugins) |
| .atSourceSpan(plugins.span), |
| ); |
| case YamlScalar(:var value): |
| if (value != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.plugins) |
| .atSourceSpan(plugins.span), |
| ); |
| } |
| } |
| } |
| |
| void _validateDiagnostics( |
| DiagnosticReporter reporter, |
| String pluginName, |
| YamlNode diagnosticsValue, |
| ) { |
| var sectionName = [ |
| AnalysisOptionsFileKeys.plugins, |
| pluginName, |
| AnalysisOptionsFileKeys.diagnostics, |
| ].join('/'); |
| if (diagnosticsValue is! YamlMap) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: sectionName) |
| .atSourceSpan(diagnosticsValue.span), |
| ); |
| return; |
| } |
| |
| diagnosticsValue.nodes.forEach((codeNameNode, severityNode) { |
| // 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 (!_ErrorFilterOptionValidator.legalValues.contains(severity)) { |
| reporter.report( |
| diag.unsupportedOptionWithLegalValues |
| .withArguments( |
| sectionName: sectionName, |
| optionKey: severityNode.value.toString(), |
| legalValues: _ErrorFilterOptionValidator.legalValueString, |
| ) |
| .atSourceSpan(severityNode.span), |
| ); |
| } |
| }); |
| } |
| |
| void _validateGit( |
| DiagnosticReporter reporter, |
| String pluginName, |
| YamlNode gitValue, |
| ) { |
| var sectionName = [ |
| AnalysisOptionsFileKeys.plugins, |
| pluginName, |
| 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; |
| } |
| |
| gitValue.nodes.forEach((keyNode, valueNode) { |
| if (keyNode case YamlScalar(value: String key)) { |
| if (!AnalysisOptionsFileKeys.gitOptions.contains(key)) { |
| _gitBuilder.reportError(reporter, sectionName, keyNode); |
| } else if (valueNode is! YamlScalar || valueNode.value is! String) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: '$sectionName/$key') |
| .atSourceSpan(valueNode.span), |
| ); |
| } |
| } |
| }); |
| } |
| |
| void _validatePluginMap( |
| DiagnosticReporter reporter, |
| String pluginName, |
| YamlMap pluginValue, |
| ) { |
| pluginValue.nodes.forEach((pluginMapKeyNode, pluginMapValueNode) { |
| if (pluginMapKeyNode case YamlScalar(value: String pluginMapKey)) { |
| if (pluginMapKey == AnalysisOptionsFileKeys.diagnostics) { |
| _validateDiagnostics(reporter, pluginName, pluginMapValueNode); |
| } else if (pluginMapKey == AnalysisOptionsFileKeys.git) { |
| _validateGit(reporter, pluginName, pluginMapValueNode); |
| } else if (!AnalysisOptionsFileKeys.pluginsOptions.contains( |
| pluginMapKey, |
| )) { |
| _builder.reportError( |
| reporter, |
| '${AnalysisOptionsFileKeys.plugins}/$pluginName', |
| pluginMapKeyNode, |
| ); |
| } |
| } |
| // TODO(srawlins): Validate 'path' is a YamlScalar. |
| }); |
| } |
| } |
| |
| /// Validates `analyzer` strong-mode value configuration options. |
| class _StrongModeOptionValueValidator extends OptionsValidator { |
| final _ErrorBuilder _builder = _ErrorBuilder( |
| AnalysisOptionsFileKeys.strongModeOptions, |
| ); |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var analyzer = options.valueAt(AnalysisOptionsFileKeys.analyzer); |
| if (analyzer is YamlMap) { |
| var strongModeNode = analyzer.valueAt(AnalysisOptionsFileKeys.strongMode); |
| if (strongModeNode is YamlMap) { |
| return _validateStrongModeAsMap(reporter, strongModeNode); |
| } else if (strongModeNode != null) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.strongMode) |
| .atSourceSpan(strongModeNode.span), |
| ); |
| } |
| } |
| } |
| |
| void _validateStrongModeAsMap( |
| DiagnosticReporter reporter, |
| YamlMap strongModeNode, |
| ) { |
| strongModeNode.nodes.forEach((k, v) { |
| if (k is YamlScalar) { |
| var key = k.value?.toString(); |
| if (!AnalysisOptionsFileKeys.strongModeOptions.contains(key)) { |
| _builder.reportError(reporter, AnalysisOptionsFileKeys.strongMode, k); |
| } else if (key == AnalysisOptionsFileKeys.declarationCasts) { |
| reporter.report( |
| diag.unsupportedValue |
| .withArguments( |
| optionName: AnalysisOptionsFileKeys.strongMode, |
| invalidValue: v.value.toString(), |
| legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal, |
| ) |
| .atSourceSpan(v.span), |
| ); |
| } else { |
| // The key is valid. |
| if (v is YamlScalar) { |
| if (v.toBool() == null) { |
| reporter.report( |
| diag.unsupportedValue |
| .withArguments( |
| optionName: key!, |
| invalidValue: v.value.toString(), |
| legalValues: AnalysisOptionsFileKeys.trueOrFalseProposal, |
| ) |
| .atSourceSpan(v.span), |
| ); |
| } |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| /// Validates top-level options. For example, |
| /// |
| /// ```yaml |
| /// analyzer: |
| /// exclude: |
| /// - lib/generated/ |
| /// ``` |
| class _TopLevelOptionValidator extends OptionsValidator { |
| final String sectionName; |
| final Set<String> supportedOptions; |
| final String _valueProposal; |
| |
| _TopLevelOptionValidator(this.sectionName, this.supportedOptions) |
| : assert(supportedOptions.isNotEmpty), |
| _valueProposal = supportedOptions.quotedAndCommaSeparatedWithAnd; |
| |
| @override |
| void validate(DiagnosticReporter reporter, YamlMap options) { |
| var node = options.valueAt(sectionName); |
| if (node == null) return; |
| if (node is YamlScalar && node.value == null) return; |
| |
| if (node is! YamlMap) { |
| reporter.report( |
| diag.invalidSectionFormat |
| .withArguments(sectionName: AnalysisOptionsFileKeys.cannotIgnore) |
| .atSourceSpan(node.span), |
| ); |
| return; |
| } |
| node.nodes.forEach((k, v) { |
| if (k is YamlScalar) { |
| if (!supportedOptions.contains(k.value)) { |
| var optionKey = k.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(k.span)); |
| } |
| } |
| // TODO(pq): consider an error if the node is not a Scalar. |
| }); |
| } |
| } |