| // Copyright (c) 2024, 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 'dart:collection'; | |
| import 'dart:convert'; | |
| import 'dart:typed_data'; | |
| import 'package:analyzer/dart/analysis/analysis_options.dart'; | |
| import 'package:analyzer/dart/analysis/code_style_options.dart'; | |
| import 'package:analyzer/dart/analysis/features.dart'; | |
| import 'package:analyzer/dart/analysis/formatter_options.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/source/error_processor.dart'; | |
| import 'package:analyzer/src/analysis_options/code_style_options.dart'; | |
| import 'package:analyzer/src/analysis_rule/rule_context.dart'; | |
| import 'package:analyzer/src/dart/analysis/experiments.dart'; | |
| import 'package:analyzer/src/lint/config.dart'; | |
| import 'package:analyzer/src/summary/api_signature.dart'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:pub_semver/pub_semver.dart'; | |
| /// A builder for [AnalysisOptionsImpl]. | |
| /// | |
| /// To be used when an [AnalysisOptionsImpl] needs to be constructed, but with | |
| /// individually set options, rather than being built from YAML. | |
| final class AnalysisOptionsBuilder { | |
| final File? file; | |
| ExperimentStatus _contextFeatures = ExperimentStatus(); | |
| FeatureSet nonPackageFeatureSet = ExperimentStatus(); | |
| List<String> enabledLegacyPluginNames = []; | |
| List<ErrorProcessor> errorProcessors = []; | |
| Set<AnalysisOptionsExcludePattern> excludePatterns = {}; | |
| bool lint = false; | |
| bool warning = true; | |
| List<AbstractAnalysisRule> lintRules = []; | |
| bool propagateLinterExceptions = false; | |
| bool strictCasts = false; | |
| bool strictInference = false; | |
| bool strictRawTypes = false; | |
| bool chromeOsManifestChecks = false; | |
| CodeStyleOptions codeStyleOptions = CodeStyleOptionsImpl(useFormatter: false); | |
| FormatterOptions formatterOptions = FormatterOptions(); | |
| Set<String> unignorableDiagnosticCodeNames = {}; | |
| PluginsOptions pluginsOptions = PluginsOptions( | |
| configurations: [], | |
| dependencyOverrides: null, | |
| ); | |
| String? pluginDependencyOverrides; | |
| /// Creates a builder initialized with default options. | |
| AnalysisOptionsBuilder({this.file}); | |
| /// Creates a builder initialized from an existing options object. | |
| AnalysisOptionsBuilder.from(AnalysisOptionsImpl options) | |
| : file = options.file { | |
| contextFeatures = options.contextFeatures; | |
| nonPackageFeatureSet = options.nonPackageFeatureSet; | |
| enabledLegacyPluginNames = options.enabledLegacyPluginNames.toList(); | |
| pluginsOptions = PluginsOptions( | |
| configurations: options.pluginsOptions.configurations.toList(), | |
| dependencyOverrides: options.pluginsOptions.dependencyOverrides == null | |
| ? null | |
| : Map.of(options.pluginsOptions.dependencyOverrides!), | |
| ); | |
| errorProcessors = options.errorProcessors.toList(); | |
| excludePatterns = options.excludePatterns2.toSet(); | |
| lint = options.lint; | |
| warning = options.warning; | |
| lintRules = options.lintRules.toList(); | |
| propagateLinterExceptions = options.propagateLinterExceptions; | |
| strictCasts = options.strictCasts; | |
| strictInference = options.strictInference; | |
| strictRawTypes = options.strictRawTypes; | |
| chromeOsManifestChecks = options.chromeOsManifestChecks; | |
| codeStyleOptions = CodeStyleOptionsImpl( | |
| useFormatter: options.codeStyleOptions.useFormatter, | |
| ); | |
| formatterOptions = FormatterOptions( | |
| pageWidth: options.formatterOptions.pageWidth, | |
| trailingCommas: options.formatterOptions.trailingCommas, | |
| ); | |
| unignorableDiagnosticCodeNames = options.unignorableDiagnosticCodeNames | |
| .toSet(); | |
| } | |
| FeatureSet get contextFeatures => _contextFeatures; | |
| set contextFeatures(FeatureSet featureSet) { | |
| _contextFeatures = featureSet as ExperimentStatus; | |
| nonPackageFeatureSet = featureSet; | |
| } | |
| AnalysisOptionsImpl build() { | |
| return AnalysisOptionsImpl._( | |
| file: file, | |
| contextFeatures: _contextFeatures, | |
| nonPackageFeatureSet: nonPackageFeatureSet, | |
| enabledLegacyPluginNames: enabledLegacyPluginNames.toList(), | |
| pluginsOptions: PluginsOptions( | |
| configurations: pluginsOptions.configurations.toList(), | |
| dependencyOverrides: pluginsOptions.dependencyOverrides == null | |
| ? null | |
| : Map.of(pluginsOptions.dependencyOverrides!), | |
| ), | |
| errorProcessors: errorProcessors.toList(), | |
| excludePatterns2: excludePatterns.toList(), | |
| lint: lint, | |
| warning: warning, | |
| lintRules: lintRules.toList(), | |
| propagateLinterExceptions: propagateLinterExceptions, | |
| strictCasts: strictCasts, | |
| strictInference: strictInference, | |
| strictRawTypes: strictRawTypes, | |
| chromeOsManifestChecks: chromeOsManifestChecks, | |
| codeStyleOptions: CodeStyleOptionsImpl( | |
| useFormatter: codeStyleOptions.useFormatter, | |
| ), | |
| formatterOptions: FormatterOptions( | |
| pageWidth: formatterOptions.pageWidth, | |
| trailingCommas: formatterOptions.trailingCommas, | |
| ), | |
| unignorableDiagnosticCodeNames: unignorableDiagnosticCodeNames.toSet(), | |
| ); | |
| } | |
| } | |
| /// An exclude pattern declared in an analysis options file. | |
| final class AnalysisOptionsExcludePattern { | |
| /// The analysis options file that declares [pattern]. | |
| final File declaringFile; | |
| /// The pattern as written in [declaringFile]. | |
| final String pattern; | |
| AnalysisOptionsExcludePattern({ | |
| required this.declaringFile, | |
| required this.pattern, | |
| }); | |
| @override | |
| int get hashCode => Object.hash(declaringFile, pattern); | |
| @override | |
| bool operator ==(Object other) { | |
| return other is AnalysisOptionsExcludePattern && | |
| other.declaringFile == declaringFile && | |
| other.pattern == pattern; | |
| } | |
| } | |
| /// A set of analysis options used to control the behavior of an analysis | |
| /// context. | |
| class AnalysisOptionsImpl implements AnalysisOptions { | |
| /// The cached [unlinkedSignature]. | |
| Uint32List? _unlinkedSignature; | |
| /// The cached [signature]. | |
| Uint32List? _signature; | |
| /// The cached [signatureForElements]. | |
| Uint32List? _signatureForElements; | |
| /// The constraint on the language version for every Dart file. | |
| /// Violations will be reported as analysis errors. | |
| final VersionConstraint? sourceLanguageConstraint = VersionConstraint.parse( | |
| '>= 2.12.0', | |
| ); | |
| @override | |
| final ExperimentStatus contextFeatures; | |
| /// The set of features to use for libraries that are not in a package. | |
| /// | |
| /// If a library is in a package, this feature set is *not* used, even if the | |
| /// package does not specify the language version. Instead [contextFeatures] | |
| /// is used. | |
| final FeatureSet nonPackageFeatureSet; | |
| @override | |
| final List<String> enabledLegacyPluginNames; | |
| /// The options used to specify plugins. | |
| final PluginsOptions pluginsOptions; | |
| @override | |
| final List<ErrorProcessor> errorProcessors; | |
| final List<AnalysisOptionsExcludePattern> excludePatterns2; | |
| /// The associated `analysis_options.yaml` file (or `null` if there is none). | |
| final File? file; | |
| @override | |
| final bool lint; | |
| @override | |
| final bool warning; | |
| @override | |
| final List<AbstractAnalysisRule> lintRules; | |
| /// Whether linter exceptions should be propagated to the caller (by | |
| /// rethrowing them). | |
| final bool propagateLinterExceptions; | |
| @override | |
| final bool strictCasts; | |
| @override | |
| final bool strictInference; | |
| @override | |
| final bool strictRawTypes; | |
| @override | |
| final bool chromeOsManifestChecks; | |
| @override | |
| final CodeStyleOptions codeStyleOptions; | |
| @override | |
| final FormatterOptions formatterOptions; | |
| /// The set of "un-ignorable" diagnostic names, as parsed from an analysis | |
| /// options file. | |
| /// | |
| /// All entries in this set are in `lower_snake_case` form. | |
| final Set<String> unignorableDiagnosticCodeNames; | |
| /// Returns a newly instantiated [AnalysisOptionsImpl]. | |
| /// | |
| /// Optionally pass [file] as the file where the YAML can be found. | |
| factory AnalysisOptionsImpl({File? file}) { | |
| var builder = AnalysisOptionsBuilder(file: file); | |
| return builder.build(); | |
| } | |
| AnalysisOptionsImpl._({ | |
| required this.file, | |
| required this.contextFeatures, | |
| required this.nonPackageFeatureSet, | |
| required this.excludePatterns2, | |
| required this.enabledLegacyPluginNames, | |
| required this.pluginsOptions, | |
| required this.errorProcessors, | |
| required this.lint, | |
| required this.warning, | |
| required this.lintRules, | |
| required this.propagateLinterExceptions, | |
| required this.strictCasts, | |
| required this.strictInference, | |
| required this.strictRawTypes, | |
| required this.chromeOsManifestChecks, | |
| required this.codeStyleOptions, | |
| required this.formatterOptions, | |
| required this.unignorableDiagnosticCodeNames, | |
| }) { | |
| assert(unignorableDiagnosticCodeNames.every((n) => n == n.toLowerCase())); | |
| (codeStyleOptions as CodeStyleOptionsImpl).options = this; | |
| } | |
| @Deprecated( | |
| "Use 'AnalysisContext.contextRoot.isAnalyzed' or " | |
| "'AnalysisContext.contextRoot.analyzedFiles' instead.", | |
| ) | |
| @override | |
| List<String> get excludePatterns => { | |
| for (var excludePattern in excludePatterns2) excludePattern.pattern, | |
| }.toList(); | |
| /// The language version to use for libraries that are not in a package. | |
| /// | |
| /// If a library is in a package, this language version is *not* used, | |
| /// even if the package does not specify the language version. | |
| Version get nonPackageLanguageVersion => ExperimentStatus.currentVersion; | |
| List<PluginConfiguration> get pluginConfigurations => | |
| pluginsOptions.configurations; | |
| Uint32List get signature { | |
| if (_signature == null) { | |
| ApiSignature buffer = ApiSignature(); | |
| // Append boolean flags. | |
| buffer.addBool(propagateLinterExceptions); | |
| buffer.addBool(strictCasts); | |
| buffer.addBool(strictInference); | |
| buffer.addBool(strictRawTypes); | |
| // Append features. | |
| buffer.addInt(ExperimentStatus.knownFeatures.length); | |
| for (var feature in ExperimentStatus.knownFeatures.values) { | |
| buffer.addBool(contextFeatures.isEnabled(feature)); | |
| } | |
| // Append error processors. | |
| buffer.addInt(errorProcessors.length); | |
| for (ErrorProcessor processor in errorProcessors.sortedBy( | |
| (processor) => processor.description, | |
| )) { | |
| buffer.addString(processor.description); | |
| } | |
| // Append lints. | |
| buffer.addInt(lintRules.length); | |
| for (var lintRule in lintRules.sortedBy((lintRule) => lintRule.name)) { | |
| buffer.addString(lintRule.name); | |
| } | |
| // Append legacy plugin names. | |
| buffer.addInt(enabledLegacyPluginNames.length); | |
| for (var enabledLegacyPluginName in enabledLegacyPluginNames.sorted()) { | |
| buffer.addString(enabledLegacyPluginName); | |
| } | |
| // Append plugin configurations. | |
| buffer.addInt(pluginConfigurations.length); | |
| for (var pluginConfiguration in pluginConfigurations.sortedBy( | |
| (pluginConfiguration) => pluginConfiguration.name, | |
| )) { | |
| buffer.addString(pluginConfiguration.name); | |
| buffer.addBool(pluginConfiguration.isEnabled); | |
| switch (pluginConfiguration.source) { | |
| case GitPluginSource source: | |
| buffer.addString(source.url); | |
| if (source.path case var path?) { | |
| buffer.addString(path); | |
| } | |
| if (source.ref case var ref?) { | |
| buffer.addString(ref); | |
| } | |
| if (source.tagPattern case var tagPattern?) { | |
| buffer.addString(tagPattern); | |
| } | |
| case PathPluginSource source: | |
| buffer.addString(source.path); | |
| case VersionedPluginSource source: | |
| buffer.addString(source.constraint); | |
| if (source.hostedUrl case var hostedUrl?) { | |
| buffer.addString(hostedUrl); | |
| } | |
| } | |
| buffer.addInt(pluginConfiguration.diagnosticConfigs.length); | |
| for (var diagnosticConfig | |
| in pluginConfiguration.diagnosticConfigs.values) { | |
| buffer.addString(diagnosticConfig.group ?? ''); | |
| buffer.addString(diagnosticConfig.name); | |
| buffer.addInt(diagnosticConfig.severity.index); | |
| } | |
| if (pluginsOptions.dependencyOverrides case var dependencyOverrides?) { | |
| for (var pluginDependencyOverrideEntry | |
| in dependencyOverrides.entries.sortedBy((entry) => entry.key)) { | |
| buffer.addString(pluginDependencyOverrideEntry.key); | |
| switch (pluginDependencyOverrideEntry.value) { | |
| case GitPluginSource source: | |
| buffer.addString(source.url); | |
| if (source.path case var path?) { | |
| buffer.addString(path); | |
| } | |
| if (source.ref case var ref?) { | |
| buffer.addString(ref); | |
| } | |
| if (source.tagPattern case var tagPattern?) { | |
| buffer.addString(tagPattern); | |
| } | |
| case PathPluginSource source: | |
| buffer.addString(source.path); | |
| case VersionedPluginSource source: | |
| buffer.addString(source.constraint); | |
| } | |
| } | |
| } | |
| } | |
| // Hash and convert to Uint32List. | |
| _signature = buffer.toUint32List(); | |
| } | |
| return _signature!; | |
| } | |
| Uint32List get signatureForElements { | |
| if (_signatureForElements == null) { | |
| ApiSignature buffer = ApiSignature(); | |
| // Append features. | |
| buffer.addInt(ExperimentStatus.knownFeatures.length); | |
| for (var feature in ExperimentStatus.knownFeatures.values) { | |
| buffer.addBool(contextFeatures.isEnabled(feature)); | |
| } | |
| // Hash and convert to Uint32List. | |
| _signatureForElements = buffer.toUint32List(); | |
| } | |
| return _signatureForElements!; | |
| } | |
| /// The opaque signature of the options that affect unlinked data. | |
| Uint32List get unlinkedSignature { | |
| if (_unlinkedSignature == null) { | |
| ApiSignature buffer = ApiSignature(); | |
| // Append the current language version. | |
| buffer.addInt(ExperimentStatus.currentVersion.major); | |
| buffer.addInt(ExperimentStatus.currentVersion.minor); | |
| // Append features. | |
| buffer.addInt(ExperimentStatus.knownFeatures.length); | |
| for (var feature in ExperimentStatus.knownFeatures.values) { | |
| buffer.addBool(contextFeatures.isEnabled(feature)); | |
| } | |
| // Hash and convert to Uint32List. | |
| return buffer.toUint32List(); | |
| } | |
| return _unlinkedSignature!; | |
| } | |
| @override | |
| bool isLintEnabled(String name) { | |
| return lintRules.any((rule) => rule.name == name); | |
| } | |
| /// Returns a "debug information" map of this set of analysis options. | |
| /// | |
| /// This map can be presented in different formats, like text in a | |
| /// terminal, or HTML. | |
| Map<String, Object> toDebugInfo() { | |
| return { | |
| 'enable-experiment': contextFeatures, | |
| 'diagnostic severities': { | |
| for (var processor in errorProcessors) | |
| processor.code: processor.severity, | |
| }, | |
| 'excludes': [ | |
| for (var excludePattern in excludePatterns2) | |
| { | |
| 'file': excludePattern.declaringFile.path, | |
| 'pattern': excludePattern.pattern, | |
| }, | |
| ], | |
| 'lint rules': lintRules.map( | |
| (e) => DebugLink(e.name, e.diagnosticCodes.first.url), | |
| ), | |
| 'strict-casts': strictCasts, | |
| 'strict-inference': strictInference, | |
| 'strict-raw-types': strictRawTypes, | |
| 'formatter': { | |
| 'page_width': ?formatterOptions.pageWidth, | |
| 'trailing_commas': ?formatterOptions.trailingCommas, | |
| }, | |
| 'chrome_os_manifest_checks': chromeOsManifestChecks, | |
| 'legacy plugins': enabledLegacyPluginNames, | |
| for (var pluginConfiguration in pluginConfigurations) | |
| // TODO(srawlins): Having a top-level 'plugins' section, and then a Map | |
| // for each plugin is way too nested in the HTML table. This | |
| // interpolated "plugins/foo" section solves that, but is a bit hacky. | |
| // It'd be nice to have a general way in the Insights HTML to have a | |
| // cell with `colspan=2` and then each plugin Map inside that. | |
| 'plugins/${pluginConfiguration.name}': { | |
| 'enabled': pluginConfiguration.isEnabled, | |
| 'diagnostics': [ | |
| for (var MapEntry(key: ruleName, value: ruleConfig) | |
| in pluginConfiguration.diagnosticConfigs.entries) | |
| {ruleName: ruleConfig.severity.name}, | |
| ], | |
| 'source': DebugCodeBlock( | |
| pluginConfiguration.source | |
| .toYaml(name: pluginConfiguration.name) | |
| .trimRight(), | |
| lang: 'yaml', | |
| ), | |
| }, | |
| }; | |
| } | |
| } | |
| /// A code block for use as "debug information." | |
| final class DebugCodeBlock { | |
| final String text; | |
| final String? lang; | |
| DebugCodeBlock(this.text, {this.lang}); | |
| } | |
| /// A debug link object, to be rendered as a Markdown link or an HTML link. | |
| /// | |
| /// If [url] is `null`, then the [text] is to be rendered as plain text. | |
| final class DebugLink { | |
| final String text; | |
| final String? url; | |
| DebugLink(this.text, this.url); | |
| } | |
| final class GitPluginSource implements PluginSource { | |
| final String url; | |
| final String? path; | |
| final String? ref; | |
| final String? tagPattern; | |
| GitPluginSource({required this.url, this.path, this.ref, this.tagPattern}); | |
| @override | |
| int get hashCode => Object.hash(url, path, ref, tagPattern); | |
| @override | |
| bool operator ==(Object other) => | |
| identical(this, other) || | |
| other is GitPluginSource && | |
| url == other.url && | |
| path == other.path && | |
| ref == other.ref && | |
| tagPattern == other.tagPattern; | |
| @override | |
| String toYaml({required String name}) { | |
| var buffer = StringBuffer() | |
| ..writeln(' $name:') | |
| ..writeln(' git:') | |
| ..writeln(' url: $url'); | |
| if (ref != null) { | |
| buffer.writeln(' ref: $ref'); | |
| } | |
| if (path != null) { | |
| buffer.writeln(' path: $path'); | |
| } | |
| if (tagPattern != null) { | |
| buffer.writeln(' tag_pattern: $tagPattern'); | |
| } | |
| return buffer.toString(); | |
| } | |
| } | |
| final class PathPluginSource implements PluginSource { | |
| final String path; | |
| PathPluginSource({required this.path}); | |
| @override | |
| int get hashCode => path.hashCode; | |
| @override | |
| bool operator ==(Object other) => | |
| identical(this, other) || other is PathPluginSource && path == other.path; | |
| @override | |
| String toYaml({required String name}) => | |
| ''' | |
| $name: | |
| path: ${jsonEncode(path)} | |
| '''; | |
| } | |
| /// The configuration of a Dart Analysis Server plugin, as specified by | |
| /// analysis options. | |
| final class PluginConfiguration { | |
| /// The name of the plugin being configured. | |
| final String name; | |
| /// The source of the plugin being configured. | |
| final PluginSource source; | |
| /// The list of specified [DiagnosticConfig]s. | |
| final Map<String, DiagnosticConfig> diagnosticConfigs; | |
| /// Whether the plugin is enabled. | |
| final bool isEnabled; | |
| PluginConfiguration({ | |
| required this.name, | |
| required this.source, | |
| Map<String, DiagnosticConfig> diagnosticConfigs = const {}, | |
| this.isEnabled = true, | |
| }) : diagnosticConfigs = LinkedHashMap( | |
| equals: _caseInsensitiveEquals, | |
| hashCode: _caseInsensitiveHash, | |
| )..addAll(diagnosticConfigs); | |
| String sourceYaml() => source.toYaml(name: name); | |
| static bool _caseInsensitiveEquals(String s1, String s2) => | |
| s1.toLowerCase() == s2.toLowerCase(); | |
| static int _caseInsensitiveHash(String s) => s.toLowerCase().hashCode; | |
| } | |
| /// The analysis options for plugins, as specified in the top-level `plugins` | |
| /// section. | |
| final class PluginsOptions { | |
| /// The list of each listed plugin's configuration. | |
| final List<PluginConfiguration> configurations; | |
| /// The dependency overrides, if specified. | |
| final Map<String, PluginSource>? dependencyOverrides; | |
| PluginsOptions({ | |
| required this.configurations, | |
| required this.dependencyOverrides, | |
| }); | |
| } | |
| /// A description of the source of a plugin. | |
| /// | |
| /// We support all of the source formats documented at | |
| /// https://dart.dev/tools/pub/dependencies. | |
| sealed class PluginSource { | |
| /// Returns the YAML-formatted source, using [name] as a key, for writing into | |
| /// a pubspec 'dependencies' section. | |
| String toYaml({required String name}); | |
| } | |
| /// A plugin source using a version constraint, hosted either at pub.dev or | |
| /// another host. | |
| final class VersionedPluginSource implements PluginSource { | |
| /// The specified version constraint. | |
| final String constraint; | |
| final String? hostedUrl; | |
| VersionedPluginSource({required this.constraint, this.hostedUrl}); | |
| @override | |
| int get hashCode => Object.hash(constraint, hostedUrl); | |
| @override | |
| bool operator ==(Object other) => | |
| identical(this, other) || | |
| other is VersionedPluginSource && | |
| constraint == other.constraint && | |
| hostedUrl == other.hostedUrl; | |
| @override | |
| String toYaml({required String name}) { | |
| if (hostedUrl == null) { | |
| return ' $name: $constraint\n'; | |
| } | |
| var buffer = StringBuffer() | |
| ..writeln(' $name:') | |
| ..writeln(' version: $constraint') | |
| ..writeln(' hosted: $hostedUrl'); | |
| return buffer.toString(); | |
| } | |
| } |