blob: f9d98f80f807e8026fe814af37610e7974d123e4 [file] [edit]
// 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: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 = [];
List<String> 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.excludePatterns.toList();
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(),
excludePatterns: 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(),
);
}
}
/// 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',
);
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.
FeatureSet nonPackageFeatureSet;
@override
final List<String> enabledLegacyPluginNames;
/// The options used to specify plugins.
final PluginsOptions pluginsOptions;
@override
final List<ErrorProcessor> errorProcessors;
@override
final List<String> excludePatterns;
/// The associated `analysis_options.yaml` file (or `null` if there is none).
final File? file;
bool _lint;
bool _warning;
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 ExperimentStatus contextFeatures,
required this.nonPackageFeatureSet,
required this.excludePatterns,
required this.enabledLegacyPluginNames,
required this.pluginsOptions,
required this.errorProcessors,
required bool lint,
required bool warning,
required List<AbstractAnalysisRule> 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,
}) : _contextFeatures = contextFeatures,
_lint = lint,
_warning = warning,
_lintRules = lintRules {
assert(unignorableDiagnosticCodeNames.every((n) => n == n.toLowerCase()));
(codeStyleOptions as CodeStyleOptionsImpl).options = this;
}
@override
FeatureSet get contextFeatures => _contextFeatures;
// TODO(scheglov): Remove these compatibility setters after clients migrate
// to AnalysisOptionsBuilder.
@Deprecated('Use AnalysisOptionsBuilder instead.')
set contextFeatures(FeatureSet featureSet) {
_contextFeatures = featureSet as ExperimentStatus;
nonPackageFeatureSet = featureSet;
_clearCachedSignatures();
}
@override
bool get lint => _lint;
@Deprecated('Use AnalysisOptionsBuilder instead.')
set lint(bool value) {
_lint = value;
_clearCachedSignatures();
}
@override
List<AbstractAnalysisRule> get lintRules => _lintRules;
@Deprecated('Use AnalysisOptionsBuilder instead.')
set lintRules(List<AbstractAnalysisRule> value) {
_lintRules = value;
_clearCachedSignatures();
}
/// 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 get warning => _warning;
@Deprecated('Use AnalysisOptionsBuilder instead.')
set warning(bool value) {
_warning = value;
_clearCachedSignatures();
}
@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': excludePatterns,
'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',
),
},
};
}
void _clearCachedSignatures() {
_unlinkedSignature = null;
_signature = null;
_signatureForElements = null;
}
}
/// 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
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
String toYaml({required String name}) =>
'''
$name:
path: $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
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();
}
}