mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-17 19:27:11 +00:00
[ruff][ext-lint] external lint rule selection
This commit is contained in:
parent
3c786d5fb2
commit
0700747939
15 changed files with 1977 additions and 12 deletions
|
|
@ -28,7 +28,7 @@ use ruff_options_metadata::{OptionEntry, OptionsMetadata};
|
|||
use ruff_python_ast as ast;
|
||||
use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::configuration::{Configuration, ExternalRuleSelection, RuleSelection};
|
||||
use ruff_workspace::options::{Options, PycodestyleOptions};
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
|
@ -462,6 +462,53 @@ pub struct CheckCommand {
|
|||
conflicts_with = "watch",
|
||||
)]
|
||||
pub show_settings: bool,
|
||||
/// List configured external AST linters and exit.
|
||||
#[arg(
|
||||
long,
|
||||
help_heading = "External linter options",
|
||||
conflicts_with = "add_noqa"
|
||||
)]
|
||||
pub list_external_linters: bool,
|
||||
/// Restrict linting to the given external linter IDs.
|
||||
#[arg(
|
||||
long = "select-external",
|
||||
value_name = "LINTER",
|
||||
action = clap::ArgAction::Append,
|
||||
help_heading = "External linter options",
|
||||
)]
|
||||
pub select_external: Vec<String>,
|
||||
/// Enable additional external linter IDs or rule codes without replacing existing selections.
|
||||
#[arg(
|
||||
long = "extend-select-external",
|
||||
value_name = "LINTER",
|
||||
action = clap::ArgAction::Append,
|
||||
help_heading = "External linter options",
|
||||
)]
|
||||
pub extend_select_external: Vec<String>,
|
||||
/// Disable the given external linter IDs or rule codes.
|
||||
#[arg(
|
||||
long = "ignore-external",
|
||||
value_name = "LINTER",
|
||||
action = clap::ArgAction::Append,
|
||||
help_heading = "External linter options",
|
||||
)]
|
||||
pub ignore_external: Vec<String>,
|
||||
/// Disable additional external linter IDs or rule codes without replacing existing ignores.
|
||||
#[arg(
|
||||
long = "extend-ignore-external",
|
||||
value_name = "LINTER",
|
||||
action = clap::ArgAction::Append,
|
||||
help_heading = "External linter options",
|
||||
)]
|
||||
pub extend_ignore_external: Vec<String>,
|
||||
/// Validate external linter definitions without running lint checks.
|
||||
#[arg(
|
||||
long = "verify-external-linters",
|
||||
help_heading = "External linter options",
|
||||
conflicts_with = "add_noqa",
|
||||
conflicts_with = "list_external_linters"
|
||||
)]
|
||||
pub verify_external_linters: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
|
|
@ -730,6 +777,70 @@ impl CheckCommand {
|
|||
self,
|
||||
global_options: GlobalConfigArgs,
|
||||
) -> anyhow::Result<(CheckArguments, ConfigArguments)> {
|
||||
if let Some(invalid) = self
|
||||
.select_external
|
||||
.iter()
|
||||
.find(|selector| is_builtin_rule_selector(selector))
|
||||
{
|
||||
anyhow::bail!(
|
||||
"Internal rule `{invalid}` cannot be enabled with `--select-external`; use `--select` instead."
|
||||
);
|
||||
}
|
||||
if let Some(selector) = self.select.as_ref().and_then(|selectors| {
|
||||
selectors.iter().find_map(|selector| {
|
||||
if let RuleSelector::External { code } = selector {
|
||||
Some(code.as_ref().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}) {
|
||||
anyhow::bail!(
|
||||
"External rule `{selector}` cannot be enabled with `--select`; use `--select-external` instead."
|
||||
);
|
||||
}
|
||||
if let Some(selector) = self.extend_select.as_ref().and_then(|selectors| {
|
||||
selectors.iter().find_map(|selector| {
|
||||
if let RuleSelector::External { code } = selector {
|
||||
Some(code.as_ref().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}) {
|
||||
anyhow::bail!(
|
||||
"External rule `{selector}` cannot be enabled with `--extend-select`; use `--extend-select-external` instead."
|
||||
);
|
||||
}
|
||||
if let Some(invalid) = self
|
||||
.extend_select_external
|
||||
.iter()
|
||||
.find(|selector| is_builtin_rule_selector(selector))
|
||||
{
|
||||
anyhow::bail!(
|
||||
"Internal rule `{invalid}` cannot be enabled with `--extend-select-external`; use `--extend-select` instead."
|
||||
);
|
||||
}
|
||||
if let Some(invalid) = self
|
||||
.ignore_external
|
||||
.iter()
|
||||
.chain(self.extend_ignore_external.iter())
|
||||
.find(|selector| is_builtin_rule_selector(selector))
|
||||
{
|
||||
anyhow::bail!(
|
||||
"Internal rule `{invalid}` cannot be disabled with `--ignore-external`; use `--ignore` instead."
|
||||
);
|
||||
}
|
||||
|
||||
let select_external_override = if self.select_external.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.select_external.clone())
|
||||
};
|
||||
let extend_select_external_override = self.extend_select_external.clone();
|
||||
let ignore_external_override = self.ignore_external.clone();
|
||||
let extend_ignore_external_override = self.extend_ignore_external.clone();
|
||||
|
||||
let check_arguments = CheckArguments {
|
||||
add_noqa: self.add_noqa,
|
||||
diff: self.diff,
|
||||
|
|
@ -741,6 +852,12 @@ impl CheckCommand {
|
|||
output_file: self.output_file,
|
||||
show_files: self.show_files,
|
||||
show_settings: self.show_settings,
|
||||
list_external_linters: self.list_external_linters,
|
||||
select_external: self.select_external,
|
||||
extend_select_external: self.extend_select_external,
|
||||
ignore_external: self.ignore_external,
|
||||
extend_ignore_external: self.extend_ignore_external,
|
||||
verify_external_linters: self.verify_external_linters,
|
||||
statistics: self.statistics,
|
||||
stdin_filename: self.stdin_filename,
|
||||
watch: self.watch,
|
||||
|
|
@ -774,6 +891,10 @@ impl CheckCommand {
|
|||
output_format: self.output_format,
|
||||
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
|
||||
extension: self.extension,
|
||||
select_external: select_external_override,
|
||||
extend_select_external: extend_select_external_override,
|
||||
ignore_external: ignore_external_override,
|
||||
extend_ignore_external: extend_ignore_external_override,
|
||||
..ExplicitConfigOverrides::default()
|
||||
};
|
||||
|
||||
|
|
@ -1072,6 +1193,12 @@ pub struct CheckArguments {
|
|||
pub output_file: Option<PathBuf>,
|
||||
pub show_files: bool,
|
||||
pub show_settings: bool,
|
||||
pub list_external_linters: bool,
|
||||
pub select_external: Vec<String>,
|
||||
pub extend_select_external: Vec<String>,
|
||||
pub ignore_external: Vec<String>,
|
||||
pub extend_ignore_external: Vec<String>,
|
||||
pub verify_external_linters: bool,
|
||||
pub statistics: bool,
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
pub watch: bool,
|
||||
|
|
@ -1335,6 +1462,10 @@ struct ExplicitConfigOverrides {
|
|||
extension: Option<Vec<ExtensionPair>>,
|
||||
detect_string_imports: Option<bool>,
|
||||
string_imports_min_dots: Option<usize>,
|
||||
select_external: Option<Vec<String>>,
|
||||
extend_select_external: Vec<String>,
|
||||
ignore_external: Vec<String>,
|
||||
extend_ignore_external: Vec<String>,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
|
|
@ -1425,11 +1556,33 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
|
|||
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
|
||||
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
|
||||
}
|
||||
if self.select_external.is_some()
|
||||
|| !self.extend_select_external.is_empty()
|
||||
|| !self.ignore_external.is_empty()
|
||||
|| !self.extend_ignore_external.is_empty()
|
||||
{
|
||||
config
|
||||
.lint
|
||||
.external_rule_selections
|
||||
.push(ExternalRuleSelection {
|
||||
select: self.select_external.clone(),
|
||||
extend_select: self.extend_select_external.clone(),
|
||||
ignore: self.ignore_external.clone(),
|
||||
extend_ignore: self.extend_ignore_external.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
fn is_builtin_rule_selector(selector: &str) -> bool {
|
||||
matches!(
|
||||
RuleSelector::from_str(selector),
|
||||
Ok(RuleSelector::Linter(_) | RuleSelector::Prefix { .. } | RuleSelector::Rule { .. })
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
|
||||
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
|
||||
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use ruff_linter::settings::{LinterSettings, flags};
|
|||
use ruff_linter::{IOError, Violation, fs, warn_user_once};
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::Settings;
|
||||
use ruff_workspace::resolver::{
|
||||
PyprojectConfig, ResolvedFile, match_exclusion, python_files_in_path,
|
||||
};
|
||||
|
|
@ -28,6 +29,7 @@ use ruff_workspace::resolver::{
|
|||
use crate::args::ConfigArguments;
|
||||
use crate::cache::{Cache, PackageCacheMap, PackageCaches};
|
||||
use crate::diagnostics::Diagnostics;
|
||||
use crate::{apply_external_linter_selection_to_settings, compute_external_selection_state};
|
||||
|
||||
/// Run the linter over a collection of files.
|
||||
pub(crate) fn check(
|
||||
|
|
@ -41,6 +43,20 @@ pub(crate) fn check(
|
|||
) -> Result<Diagnostics> {
|
||||
// Collect all the Python files to check.
|
||||
let start = Instant::now();
|
||||
let apply_external_selection = |settings: &mut Settings| -> Result<()> {
|
||||
let state = compute_external_selection_state(
|
||||
&settings.linter.selected_external,
|
||||
&settings.linter.ignored_external,
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
settings.linter.selected_external = state.effective.iter().cloned().collect();
|
||||
settings.linter.ignored_external = state.ignored.iter().cloned().collect();
|
||||
apply_external_linter_selection_to_settings(settings, &state.effective, &state.ignored)?;
|
||||
Ok(())
|
||||
};
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?;
|
||||
debug!("Identified files to lint in: {:?}", start.elapsed());
|
||||
|
||||
|
|
@ -49,6 +65,17 @@ pub(crate) fn check(
|
|||
return Ok(Diagnostics::default());
|
||||
}
|
||||
|
||||
let resolver = resolver.transform_settings(apply_external_selection)?;
|
||||
let any_external_selection = resolver
|
||||
.settings()
|
||||
.any(|settings| !settings.linter.selected_external.is_empty());
|
||||
let any_external_registry = resolver
|
||||
.settings()
|
||||
.any(|settings| settings.linter.external_ast.is_some());
|
||||
if any_external_selection && !any_external_registry {
|
||||
anyhow::bail!("No external AST linters are configured in this workspace.");
|
||||
}
|
||||
|
||||
// Discover the package root for each Python file.
|
||||
let package_roots = resolver.package_roots(
|
||||
&paths
|
||||
|
|
|
|||
|
|
@ -596,7 +596,7 @@ impl<'a> FormatResults<'a> {
|
|||
.iter()
|
||||
.map(Diagnostic::from)
|
||||
.chain(self.to_diagnostics(&mut notebook_index))
|
||||
.sorted_unstable_by(Diagnostic::ruff_start_ordering)
|
||||
.sorted_by(Diagnostic::ruff_start_ordering)
|
||||
.collect();
|
||||
|
||||
let context = EmitterContext::new(¬ebook_index);
|
||||
|
|
|
|||
293
crates/ruff/src/external.rs
Normal file
293
crates/ruff/src/external.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
use std::io::{self, Write};
|
||||
|
||||
use anyhow::Result;
|
||||
use ruff_linter::external::ExternalLintRegistry;
|
||||
use ruff_linter::external::ast::rule::{ExternalAstLinter, ExternalAstRule};
|
||||
use ruff_linter::registry::Rule;
|
||||
use ruff_workspace::Settings;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExternalSelectionState {
|
||||
pub ignored: FxHashSet<String>,
|
||||
pub effective: FxHashSet<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn compute_external_selection_state(
|
||||
base_selected: &[String],
|
||||
base_ignored: &[String],
|
||||
cli_select: &[String],
|
||||
cli_extend_select: &[String],
|
||||
cli_ignore: &[String],
|
||||
cli_extend_ignore: &[String],
|
||||
) -> ExternalSelectionState {
|
||||
let mut selected: FxHashSet<String> = if cli_select.is_empty() {
|
||||
base_selected.iter().cloned().collect()
|
||||
} else {
|
||||
FxHashSet::default()
|
||||
};
|
||||
selected.extend(cli_select.iter().cloned());
|
||||
selected.extend(cli_extend_select.iter().cloned());
|
||||
|
||||
let mut ignored: FxHashSet<String> = base_ignored.iter().cloned().collect();
|
||||
ignored.extend(cli_ignore.iter().cloned());
|
||||
ignored.extend(cli_extend_ignore.iter().cloned());
|
||||
|
||||
let effective = selected
|
||||
.iter()
|
||||
.filter(|code| !ignored.contains(*code))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
ExternalSelectionState { ignored, effective }
|
||||
}
|
||||
|
||||
pub(crate) fn apply_external_linter_selection_to_settings(
|
||||
settings: &mut Settings,
|
||||
selected: &FxHashSet<String>,
|
||||
ignored: &FxHashSet<String>,
|
||||
) -> Result<bool> {
|
||||
let linter = &mut settings.linter;
|
||||
|
||||
if selected.is_empty() {
|
||||
if linter.rules.enabled(Rule::ExternalLinter) {
|
||||
linter.rules.disable(Rule::ExternalLinter);
|
||||
}
|
||||
linter.selected_external.clear();
|
||||
linter.external_ast = None;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some(registry) = linter.external_ast.take() {
|
||||
let selection = select_external_linters(®istry, selected, ignored);
|
||||
if !selection.missing.is_empty() {
|
||||
anyhow::bail!(
|
||||
"Unknown external linter or rule selector(s): {}",
|
||||
selection.missing.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
let mut filtered = ExternalLintRegistry::new();
|
||||
for matched in &selection.matches {
|
||||
filtered.insert_linter(matched.clone_selected())?;
|
||||
}
|
||||
|
||||
linter.selected_external = selected.iter().cloned().collect();
|
||||
let codes: Vec<String> = filtered
|
||||
.iter_enabled_rules()
|
||||
.map(|rule| rule.code.as_str().to_string())
|
||||
.collect();
|
||||
|
||||
if filtered.is_empty() {
|
||||
linter.rules.disable(Rule::ExternalLinter);
|
||||
linter.external_ast = None;
|
||||
linter.selected_external.clear();
|
||||
} else {
|
||||
linter.rules.enable(Rule::ExternalLinter, false);
|
||||
linter.external_ast = Some(filtered);
|
||||
let external_codes = &mut linter.external;
|
||||
for code in codes {
|
||||
if !external_codes.iter().any(|existing| existing == &code) {
|
||||
external_codes.push(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
linter.external_ast = None;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_external_linters<'a>(
|
||||
registry: &'a ExternalLintRegistry,
|
||||
selected: &FxHashSet<String>,
|
||||
ignored: &FxHashSet<String>,
|
||||
) -> SelectedExternalLinters<'a> {
|
||||
let mut matches = Vec::new();
|
||||
let mut missing = Vec::new();
|
||||
|
||||
let enabled_linters: Vec<&'a ExternalAstLinter> = registry
|
||||
.linters()
|
||||
.iter()
|
||||
.filter(|linter| linter.enabled)
|
||||
.collect();
|
||||
|
||||
if selected.is_empty() {
|
||||
matches.extend(
|
||||
enabled_linters
|
||||
.iter()
|
||||
.copied()
|
||||
.map(SelectedExternalLinter::all_rules),
|
||||
);
|
||||
return SelectedExternalLinters { matches, missing };
|
||||
}
|
||||
|
||||
let mut satisfied: FxHashSet<&'a str> = FxHashSet::default();
|
||||
let mut available_linter_ids: FxHashSet<&'a str> = FxHashSet::default();
|
||||
|
||||
for linter in &enabled_linters {
|
||||
available_linter_ids.insert(linter.id.as_str());
|
||||
}
|
||||
|
||||
for linter in enabled_linters {
|
||||
let selected_linter = selected.contains(linter.id.as_str());
|
||||
|
||||
if selected_linter && ignored.is_empty() {
|
||||
matches.push(SelectedExternalLinter::all_rules(linter));
|
||||
satisfied.insert(linter.id.as_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
let included: Vec<_> = linter
|
||||
.rules
|
||||
.iter()
|
||||
.filter(|rule| !ignored.contains(rule.code.as_str()))
|
||||
.collect();
|
||||
|
||||
if selected_linter {
|
||||
if included.is_empty() {
|
||||
missing.push(linter.id.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
satisfied.insert(linter.id.as_str());
|
||||
for rule in &included {
|
||||
if selected.contains(rule.code.as_str()) {
|
||||
satisfied.insert(rule.code.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
matches.push(SelectedExternalLinter::subset(linter, included));
|
||||
continue;
|
||||
}
|
||||
|
||||
let matched_rules: Vec<_> = included
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|rule| selected.contains(rule.code.as_str()))
|
||||
.collect();
|
||||
|
||||
if matched_rules.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for rule in &matched_rules {
|
||||
satisfied.insert(rule.code.as_str());
|
||||
}
|
||||
|
||||
matches.push(SelectedExternalLinter::subset(linter, matched_rules));
|
||||
}
|
||||
|
||||
for selector in selected {
|
||||
let selector = selector.as_str();
|
||||
if ignored.contains(selector) || satisfied.contains(selector) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if available_linter_ids.contains(selector) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if registry.find_rule_by_code(selector).is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
missing.push(selector.to_string());
|
||||
}
|
||||
|
||||
SelectedExternalLinters { matches, missing }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SelectedExternalLinters<'a> {
|
||||
pub matches: Vec<SelectedExternalLinter<'a>>,
|
||||
pub missing: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn print_external_linters(
|
||||
registry: &ExternalLintRegistry,
|
||||
linters: &[SelectedExternalLinter<'_>],
|
||||
mut writer: impl Write,
|
||||
) -> io::Result<()> {
|
||||
match (registry.is_empty(), linters.is_empty()) {
|
||||
(true, _) => writeln!(writer, "No external AST linters configured.")?,
|
||||
(false, true) => writeln!(writer, "No matching external AST linters found.")?,
|
||||
(false, false) => {
|
||||
for selected in linters {
|
||||
selected.print(&mut writer)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SelectedExternalLinter<'a> {
|
||||
linter: &'a ExternalAstLinter,
|
||||
selection: SelectedRules<'a>,
|
||||
}
|
||||
|
||||
impl<'a> SelectedExternalLinter<'a> {
|
||||
fn all_rules(linter: &'a ExternalAstLinter) -> Self {
|
||||
Self {
|
||||
linter,
|
||||
selection: SelectedRules::All,
|
||||
}
|
||||
}
|
||||
|
||||
fn subset(linter: &'a ExternalAstLinter, rules: Vec<&'a ExternalAstRule>) -> Self {
|
||||
debug_assert!(!rules.is_empty());
|
||||
Self {
|
||||
linter,
|
||||
selection: SelectedRules::Subset(rules),
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_selected(&self) -> ExternalAstLinter {
|
||||
match &self.selection {
|
||||
SelectedRules::All => self.linter.clone(),
|
||||
SelectedRules::Subset(rules) => ExternalAstLinter {
|
||||
id: self.linter.id.clone(),
|
||||
name: self.linter.name.clone(),
|
||||
description: self.linter.description.clone(),
|
||||
enabled: self.linter.enabled,
|
||||
rules: rules.iter().map(|&rule| rule.clone()).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn print(&self, writer: &mut impl Write) -> io::Result<()> {
|
||||
match &self.selection {
|
||||
SelectedRules::All => write!(writer, "{}", self.linter),
|
||||
SelectedRules::Subset(rules) => {
|
||||
writeln!(
|
||||
writer,
|
||||
"{}{}",
|
||||
self.linter.id,
|
||||
if self.linter.enabled {
|
||||
""
|
||||
} else {
|
||||
" (disabled)"
|
||||
}
|
||||
)?;
|
||||
writeln!(writer, " name: {}", self.linter.name)?;
|
||||
if let Some(description) = &self.linter.description {
|
||||
writeln!(writer, " description: {description}")?;
|
||||
}
|
||||
writeln!(writer, " rules:")?;
|
||||
for rule in rules {
|
||||
writeln!(writer, " - {} ({})", rule.code.as_str(), rule.name)?;
|
||||
}
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SelectedRules<'a> {
|
||||
All,
|
||||
Subset(Vec<&'a ExternalAstRule>),
|
||||
}
|
||||
|
|
@ -14,26 +14,35 @@ use notify::{RecursiveMode, Watcher, recommended_watcher};
|
|||
|
||||
use args::{GlobalConfigArgs, ServerCommand};
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use ruff_linter::external::ExternalLintRegistry;
|
||||
use ruff_linter::logging::{LogLevel, set_up_logging};
|
||||
use ruff_linter::settings::flags::FixMode;
|
||||
use ruff_linter::settings::types::OutputFormat;
|
||||
use ruff_linter::{fs, warn_user, warn_user_once};
|
||||
use ruff_workspace::Settings;
|
||||
use ruff_workspace::resolver::PyprojectConfig;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::args::{
|
||||
AnalyzeCommand, AnalyzeGraphCommand, Args, CheckCommand, Command, FormatCommand,
|
||||
use crate::{
|
||||
args::{AnalyzeCommand, AnalyzeGraphCommand, Args, CheckCommand, Command, FormatCommand},
|
||||
printer::{Flags as PrinterFlags, Printer},
|
||||
};
|
||||
use crate::printer::{Flags as PrinterFlags, Printer};
|
||||
|
||||
pub mod args;
|
||||
mod cache;
|
||||
mod commands;
|
||||
mod diagnostics;
|
||||
mod external;
|
||||
mod printer;
|
||||
pub mod resolve;
|
||||
mod stdin;
|
||||
mod version;
|
||||
|
||||
pub(crate) use external::{
|
||||
apply_external_linter_selection_to_settings, compute_external_selection_state,
|
||||
print_external_linters, select_external_linters,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ExitStatus {
|
||||
/// Linting was successful and there were no linting errors.
|
||||
|
|
@ -125,6 +134,14 @@ fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_external_linter_selection(
|
||||
pyproject_config: &mut PyprojectConfig,
|
||||
selected: &FxHashSet<String>,
|
||||
ignored: &FxHashSet<String>,
|
||||
) -> Result<bool> {
|
||||
apply_external_linter_selection_to_settings(&mut pyproject_config.settings, selected, ignored)
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
Args {
|
||||
command,
|
||||
|
|
@ -238,7 +255,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
|
||||
// Construct the "default" settings. These are used when no `pyproject.toml`
|
||||
// files are present, or files are injected from outside of the hierarchy.
|
||||
let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
|
||||
let mut pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
|
||||
|
||||
let mut writer: Box<dyn Write> = match cli.output_file {
|
||||
Some(path) if !cli.watch => {
|
||||
|
|
@ -256,6 +273,48 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
|
||||
let files = resolve_default_files(cli.files, is_stdin);
|
||||
|
||||
let mut external_state = compute_external_selection_state(
|
||||
&pyproject_config.settings.linter.selected_external,
|
||||
&pyproject_config.settings.linter.ignored_external,
|
||||
&cli.select_external,
|
||||
&cli.extend_select_external,
|
||||
&cli.ignore_external,
|
||||
&cli.extend_ignore_external,
|
||||
);
|
||||
pyproject_config.settings.linter.selected_external =
|
||||
external_state.effective.iter().cloned().collect();
|
||||
pyproject_config.settings.linter.ignored_external =
|
||||
external_state.ignored.iter().cloned().collect();
|
||||
if cli.list_external_linters {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
if let Some(registry) = pyproject_config.settings.linter.external_ast.as_ref() {
|
||||
let selection = select_external_linters(
|
||||
registry,
|
||||
&external_state.effective,
|
||||
&external_state.ignored,
|
||||
);
|
||||
if !selection.missing.is_empty() {
|
||||
anyhow::bail!(
|
||||
"Unknown external linter or rule selector(s): {}",
|
||||
selection.missing.join(", ")
|
||||
);
|
||||
}
|
||||
print_external_linters(registry, &selection.matches, &mut stdout)?;
|
||||
} else {
|
||||
if !external_state.effective.is_empty() {
|
||||
anyhow::bail!("No external AST linters are configured in this workspace.");
|
||||
}
|
||||
print_external_linters(&ExternalLintRegistry::new(), &[], &mut stdout)?;
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let _ = apply_external_linter_selection(
|
||||
&mut pyproject_config,
|
||||
&external_state.effective,
|
||||
&external_state.ignored,
|
||||
)?;
|
||||
|
||||
if cli.show_settings {
|
||||
commands::show_settings::show_settings(
|
||||
&files,
|
||||
|
|
@ -275,6 +334,38 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if cli.verify_external_linters {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
if let Some(registry) = pyproject_config.settings.linter.external_ast.as_ref() {
|
||||
let selection = select_external_linters(
|
||||
registry,
|
||||
&external_state.effective,
|
||||
&external_state.ignored,
|
||||
);
|
||||
if !selection.missing.is_empty() {
|
||||
anyhow::bail!(
|
||||
"Unknown external linter or rule selector(s): {}",
|
||||
selection.missing.join(", ")
|
||||
);
|
||||
}
|
||||
if selection.matches.is_empty() {
|
||||
writeln!(stdout, "No external AST linters to validate.")?;
|
||||
} else {
|
||||
writeln!(
|
||||
stdout,
|
||||
"Validated {} external AST linter(s).",
|
||||
selection.matches.len()
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
if !external_state.effective.is_empty() {
|
||||
anyhow::bail!("No external AST linters are configured in this workspace.");
|
||||
}
|
||||
writeln!(stdout, "No external AST linters are configured.")?;
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
// Extract options that are included in `Settings`, but only apply at the top
|
||||
// level.
|
||||
let Settings {
|
||||
|
|
@ -404,6 +495,23 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
|||
if matches!(change_kind, ChangeKind::Configuration) {
|
||||
pyproject_config =
|
||||
resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
|
||||
external_state = compute_external_selection_state(
|
||||
&pyproject_config.settings.linter.selected_external,
|
||||
&pyproject_config.settings.linter.ignored_external,
|
||||
&cli.select_external,
|
||||
&cli.extend_select_external,
|
||||
&cli.ignore_external,
|
||||
&cli.extend_ignore_external,
|
||||
);
|
||||
pyproject_config.settings.linter.selected_external =
|
||||
external_state.effective.iter().cloned().collect();
|
||||
pyproject_config.settings.linter.ignored_external =
|
||||
external_state.ignored.iter().cloned().collect();
|
||||
let _ = apply_external_linter_selection(
|
||||
&mut pyproject_config,
|
||||
&external_state.effective,
|
||||
&external_state.ignored,
|
||||
)?;
|
||||
}
|
||||
Printer::clear_screen()?;
|
||||
printer.write_to_user("File change detected...\n");
|
||||
|
|
@ -514,6 +622,81 @@ https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
|
|||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod external_selection_tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ruff_linter::external::ast::registry::ExternalLintRegistry;
|
||||
use ruff_linter::external::ast::rule::{
|
||||
ExternalAstLinter, ExternalAstRule, ExternalRuleCode, ExternalRuleScript,
|
||||
};
|
||||
use ruff_linter::external::ast::target::{AstTarget, StmtKind};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::{Settings, apply_external_linter_selection_to_settings};
|
||||
|
||||
fn make_registry() -> ExternalLintRegistry {
|
||||
let mut registry = ExternalLintRegistry::new();
|
||||
|
||||
let rules = vec![
|
||||
ExternalAstRule::new(
|
||||
ExternalRuleCode::new("EXT001").unwrap(),
|
||||
"FirstRule",
|
||||
None::<&str>,
|
||||
vec![AstTarget::Stmt(StmtKind::FunctionDef)],
|
||||
ExternalRuleScript::file(
|
||||
PathBuf::from("ext001.py"),
|
||||
"def check_stmt(node, ctx):\n pass\n",
|
||||
),
|
||||
None,
|
||||
),
|
||||
ExternalAstRule::new(
|
||||
ExternalRuleCode::new("EXT002").unwrap(),
|
||||
"SecondRule",
|
||||
None::<&str>,
|
||||
vec![AstTarget::Stmt(StmtKind::FunctionDef)],
|
||||
ExternalRuleScript::file(
|
||||
PathBuf::from("ext002.py"),
|
||||
"def check_stmt(node, ctx):\n pass\n",
|
||||
),
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
let linter = ExternalAstLinter::new("demo", "Demo", None::<&str>, true, rules);
|
||||
registry.insert_linter(linter).unwrap();
|
||||
registry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selecting_linter_respects_ignored_rule_codes() {
|
||||
let registry = make_registry();
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings.linter.external_ast = Some(registry);
|
||||
settings.linter.selected_external = vec!["demo".to_string()];
|
||||
settings.linter.ignored_external = vec!["EXT002".to_string()];
|
||||
|
||||
let selected: FxHashSet<String> =
|
||||
settings.linter.selected_external.iter().cloned().collect();
|
||||
let ignored: FxHashSet<String> = settings.linter.ignored_external.iter().cloned().collect();
|
||||
apply_external_linter_selection_to_settings(&mut settings, &selected, &ignored).unwrap();
|
||||
|
||||
let filtered = settings
|
||||
.linter
|
||||
.external_ast
|
||||
.expect("external registry should remain configured");
|
||||
assert!(
|
||||
filtered.find_rule_by_code("EXT002").is_none(),
|
||||
"ignored rule code should be excluded when selecting the entire linter"
|
||||
);
|
||||
assert!(
|
||||
filtered.find_rule_by_code("EXT001").is_some(),
|
||||
"other rules should remain enabled"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_file_change_detector {
|
||||
use std::path::PathBuf;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,39 @@ impl CliTest {
|
|||
}
|
||||
}
|
||||
|
||||
fn write_demo_external_linter(test: &CliTest) -> Result<std::path::PathBuf> {
|
||||
test.write_file(
|
||||
"lint/external/demo.toml",
|
||||
r#"
|
||||
name = "Demo External Linter"
|
||||
description = "Shows how to configure external AST linters"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT001"
|
||||
name = "ExampleRule"
|
||||
summary = "Provides illustrative coverage"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT002"
|
||||
name = "AnotherRule"
|
||||
summary = "Provides illustrative coverage"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
"#,
|
||||
)?;
|
||||
test.write_file(
|
||||
"lint/external/rules/example.py",
|
||||
r#"
|
||||
def check_stmt(node, ctx):
|
||||
if node["_kind"] == "FunctionDef":
|
||||
ctx.report("external lint fired")
|
||||
"#,
|
||||
)?;
|
||||
Ok(test.root().join("lint/external/demo.toml"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_options() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
|
|
@ -168,6 +201,700 @@ inline-quotes = "single"
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_linter_listing() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file(
|
||||
"lint/external/demo.toml",
|
||||
r#"
|
||||
name = "Demo External Linter"
|
||||
description = "Shows how to configure external AST linters"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT001"
|
||||
name = "ExampleRule"
|
||||
summary = "Provides illustrative coverage"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT002"
|
||||
name = "AnotherRule"
|
||||
summary = "Demonstrates code-based selection"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
"#,
|
||||
)?;
|
||||
test.write_file(
|
||||
"lint/external/rules/example.py",
|
||||
r#"
|
||||
def check():
|
||||
# placeholder script body
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
let linter_path = test.root().join("lint/external/demo.toml");
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--config", "ruff.toml", "--list-external-linters"])
|
||||
.output()?;
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
assert!(stdout.contains("demo"));
|
||||
assert!(stdout.contains("Demo External Linter"));
|
||||
assert!(stdout.contains("EXT001"));
|
||||
assert!(stdout.contains("ExampleRule"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_linter_listing_filtered_by_code() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file(
|
||||
"lint/external/demo.toml",
|
||||
r#"
|
||||
name = "Demo External Linter"
|
||||
description = "Shows how to configure external AST linters"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT001"
|
||||
name = "ExampleRule"
|
||||
summary = "Provides illustrative coverage"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT002"
|
||||
name = "AnotherRule"
|
||||
summary = "Demonstrates code-based selection"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
"#,
|
||||
)?;
|
||||
test.write_file(
|
||||
"lint/external/rules/example.py",
|
||||
r#"
|
||||
def check():
|
||||
# placeholder script body
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
let linter_path = test.root().join("lint/external/demo.toml");
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--config",
|
||||
"ruff.toml",
|
||||
"--list-external-linters",
|
||||
"--select-external",
|
||||
"EXT002",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
assert!(stdout.contains("EXT002"));
|
||||
assert!(!stdout.contains("EXT001"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_requires_explicit_selection() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file(
|
||||
"lint/external/demo.toml",
|
||||
r#"
|
||||
name = "Demo External Linter"
|
||||
description = "Shows how to configure external AST linters"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT001"
|
||||
name = "ExampleRule"
|
||||
summary = "Provides illustrative coverage"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
"#,
|
||||
)?;
|
||||
test.write_file(
|
||||
"lint/external/rules/example.py",
|
||||
r#"
|
||||
def check():
|
||||
# placeholder script body
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
let linter_path = test.root().join("lint/external/demo.toml");
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--config", "ruff.toml", "--show-settings"])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
assert!(
|
||||
!stdout.contains("external-linter (RUF300)"),
|
||||
"Expected external-linter to be disabled without an explicit selection"
|
||||
);
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--config",
|
||||
"ruff.toml",
|
||||
"--select-external",
|
||||
"EXT001",
|
||||
"--show-settings",
|
||||
])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
assert!(
|
||||
stdout.contains("external-linter (RUF300)"),
|
||||
"Expected external-linter to be enabled when provided via --select-external"
|
||||
);
|
||||
|
||||
let config_with_select = format!(
|
||||
r#"
|
||||
[lint]
|
||||
select-external = ["EXT001"]
|
||||
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config_with_select)?;
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--config", "ruff.toml", "--show-settings"])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
assert!(
|
||||
stdout.contains("external-linter (RUF300)"),
|
||||
"Expected external-linter to be enabled when configured via lint.select-external"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_select_external_disabled_rule_errors() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
let linter_path = {
|
||||
test.write_file(
|
||||
"lint/external/disabled.toml",
|
||||
r#"
|
||||
enabled = false
|
||||
name = "Disabled External Linter"
|
||||
|
||||
[[rule]]
|
||||
code = "EXTDIS001"
|
||||
name = "DisabledRule"
|
||||
summary = "Rule intentionally disabled"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/disabled.py"
|
||||
"#,
|
||||
)?;
|
||||
test.write_file(
|
||||
"lint/external/rules/disabled.py",
|
||||
r#"
|
||||
def check_stmt(node, ctx):
|
||||
ctx.report("should not fire")
|
||||
"#,
|
||||
)?;
|
||||
test.root().join("lint/external/disabled.toml")
|
||||
};
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint.external-ast.disabled]
|
||||
path = "{}"
|
||||
|
||||
[lint]
|
||||
select-external = ["EXTDIS001"]
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
test.write_file("example.py", "def foo():\n pass\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--config", "ruff.toml", "example.py"])
|
||||
.output()?;
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"command unexpectedly succeeded: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
assert!(
|
||||
stderr.contains("Unknown external linter or rule selector(s): EXTDIS001"),
|
||||
"stderr missing warning about disabled external selection: {stderr}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_ignore_external_cli() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
let linter_path = write_demo_external_linter(&test)?;
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint]
|
||||
select-external = ["EXT001"]
|
||||
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
test.write_file(
|
||||
"example.py",
|
||||
r#"
|
||||
def demo():
|
||||
return 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--config",
|
||||
"ruff.toml",
|
||||
"--select",
|
||||
"RUF300",
|
||||
"--extend-select-external",
|
||||
"EXT002",
|
||||
"--ignore-external",
|
||||
"EXT002",
|
||||
"--list-external-linters",
|
||||
"example.py",
|
||||
])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("EXT001"), "stdout missing EXT001: {stdout}");
|
||||
assert!(
|
||||
!stdout.contains("EXT002"),
|
||||
"stdout unexpectedly included EXT002: {stdout}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_select_external_overrides_config() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
let linter_path = write_demo_external_linter(&test)?;
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint]
|
||||
select-external = ["EXT001"]
|
||||
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
test.write_file("example.py", "def demo():\n return 1\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--config",
|
||||
"ruff.toml",
|
||||
"--select",
|
||||
"RUF300",
|
||||
"--select-external",
|
||||
"EXT002",
|
||||
"--list-external-linters",
|
||||
"example.py",
|
||||
])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("EXT002"), "stdout missing EXT002: {stdout}");
|
||||
assert!(
|
||||
!stdout.contains("EXT001"),
|
||||
"stdout unexpectedly included EXT001: {stdout}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_select_external_overrides_nested_config() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
let linter_path = write_demo_external_linter(&test)?;
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint]
|
||||
select-external = ["EXT001", "EXT002"]
|
||||
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
test.write_file("nested/ruff.toml", r#"extend = "../ruff.toml""#)?;
|
||||
test.write_file(
|
||||
"nested/example.py",
|
||||
r#"
|
||||
def demo():
|
||||
return 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--select",
|
||||
"RUF300",
|
||||
"--select-external",
|
||||
"EXT002",
|
||||
"--list-external-linters",
|
||||
"nested/example.py",
|
||||
])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("EXT002"), "stdout missing EXT002: {stdout}");
|
||||
assert!(
|
||||
!stdout.contains("EXT001"),
|
||||
"stdout unexpectedly included EXT001: {stdout}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_external_cli_allows_nested_registry() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
let linter_path = write_demo_external_linter(&test)?;
|
||||
|
||||
let nested_config = format!(
|
||||
r#"
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("nested/ruff.toml", &nested_config)?;
|
||||
test.write_file("example.py", "def root():\n return 1\n")?;
|
||||
test.write_file("nested/example.py", "def nested():\n return 1\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--select",
|
||||
"RUF300",
|
||||
"--select-external",
|
||||
"EXT001",
|
||||
"example.py",
|
||||
"nested/example.py",
|
||||
])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed unexpectedly: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_external_cli_errors_when_no_registries() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file("example.py", "def root():\n return 1\n")?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--select-external", "EXT001", "example.py"])
|
||||
.output()?;
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"command unexpectedly succeeded: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("No external AST linters are configured in this workspace."),
|
||||
"stderr missing missing-registry error: {stderr}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_ignore_external_config() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
let linter_path = write_demo_external_linter(&test)?;
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint]
|
||||
select-external = ["EXT001"]
|
||||
extend-select-external = ["EXT002"]
|
||||
ignore-external = ["EXT002"]
|
||||
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
test.write_file(
|
||||
"example.py",
|
||||
r#"
|
||||
def demo():
|
||||
return 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--config",
|
||||
"ruff.toml",
|
||||
"--select",
|
||||
"RUF300",
|
||||
"--list-external-linters",
|
||||
"example.py",
|
||||
])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("EXT001"), "stdout missing EXT001: {stdout}");
|
||||
assert!(
|
||||
!stdout.contains("EXT002"),
|
||||
"stdout unexpectedly included EXT002: {stdout}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_ast_ignore_external_nested_config() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
let linter_path = write_demo_external_linter(&test)?;
|
||||
let config = format!(
|
||||
r#"
|
||||
[lint]
|
||||
select-external = ["EXT001", "EXT002"]
|
||||
ignore-external = ["EXT002"]
|
||||
|
||||
[lint.external-ast.demo]
|
||||
path = "{}"
|
||||
"#,
|
||||
linter_path.display()
|
||||
);
|
||||
test.write_file("ruff.toml", &config)?;
|
||||
test.write_file(
|
||||
"pkg/pyproject.toml",
|
||||
r#"
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
"#,
|
||||
)?;
|
||||
test.write_file(
|
||||
"pkg/example.py",
|
||||
r#"
|
||||
def demo():
|
||||
return 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args([
|
||||
"check",
|
||||
"--config",
|
||||
"ruff.toml",
|
||||
"--select",
|
||||
"RUF300",
|
||||
"--list-external-linters",
|
||||
"pkg/example.py",
|
||||
])
|
||||
.output()?;
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("EXT001"),
|
||||
"stdout missing surviving EXT001 listing: {stdout}"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("EXT002"),
|
||||
"stdout unexpectedly included ignored EXT002 listing: {stdout}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_rejects_external_rules() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--isolated", "--select", "EXT001"])
|
||||
.output()?;
|
||||
assert!(!output.status.success(), "command unexpectedly succeeded");
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
assert!(
|
||||
stderr.contains("cannot be enabled with `--select`"),
|
||||
"expected failure when selecting external rule via --select"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_external_rejects_internal_rules() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--isolated", "--select-external", "F401"])
|
||||
.output()?;
|
||||
assert!(!output.status.success(), "command unexpectedly succeeded");
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
assert!(
|
||||
stderr.contains("cannot be enabled with `--select-external`"),
|
||||
"expected failure when selecting internal rules via --select-external"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_select_rejects_external_rules() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file(
|
||||
"ruff.toml",
|
||||
r#"
|
||||
[lint]
|
||||
select = ["EXT001"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--config", "ruff.toml"])
|
||||
.output()?;
|
||||
assert!(!output.status.success(), "command unexpectedly succeeded");
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
assert!(
|
||||
stderr.contains("lint.select-external"),
|
||||
"expected parse failure when selecting external rule via configuration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_select_external_rejects_internal_rules() -> Result<()> {
|
||||
let test = CliTest::new()?;
|
||||
test.write_file(
|
||||
"ruff.toml",
|
||||
r#"
|
||||
[lint]
|
||||
select-external = ["F401"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let output = test
|
||||
.command()
|
||||
.args(["check", "--config", "ruff.toml"])
|
||||
.output()?;
|
||||
assert!(!output.status.success(), "command unexpectedly succeeded");
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
assert!(
|
||||
stderr.contains("cannot be enabled via `lint.select-external`"),
|
||||
"expected lint.select-external failure when selecting internal rule"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() -> Result<()> {
|
||||
let case = CliTest::new()?;
|
||||
|
|
@ -775,7 +1502,7 @@ fn valid_toml_but_nonexistent_option_provided_via_config_argument() {
|
|||
|
||||
Could not parse the supplied argument as a `ruff.toml` configuration option:
|
||||
|
||||
Unknown rule selector: `F481`. External rule selectors are not supported yet.
|
||||
Unknown rule selector: `F481`. External rule selectors must be provided via `lint.select-external`.
|
||||
|
||||
For more information, try '--help'.
|
||||
");
|
||||
|
|
@ -907,6 +1634,10 @@ fn value_given_to_table_key_is_not_inline_table_2() {
|
|||
- `lint.dummy-variable-rgx`
|
||||
- `lint.extend-ignore`
|
||||
- `lint.extend-select`
|
||||
- `lint.select-external`
|
||||
- `lint.extend-select-external`
|
||||
- `lint.ignore-external`
|
||||
- `lint.extend-ignore-external`
|
||||
- `lint.extend-fixable`
|
||||
- `lint.external`
|
||||
- `lint.fixable`
|
||||
|
|
|
|||
37
crates/ruff_linter/src/external/ast/registry.rs
vendored
37
crates/ruff_linter/src/external/ast/registry.rs
vendored
|
|
@ -120,6 +120,43 @@ impl ExternalLintRegistry {
|
|||
let rule = linter.rules.get(locator.rule_index)?;
|
||||
Some((rule, linter))
|
||||
}
|
||||
|
||||
pub fn iter_enabled_rules(&self) -> impl Iterator<Item = &ExternalAstRule> {
|
||||
self.linters
|
||||
.iter()
|
||||
.filter(|linter| linter.enabled)
|
||||
.flat_map(|linter| linter.rules.iter())
|
||||
}
|
||||
|
||||
pub fn iter_enabled_linter_rules(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&ExternalAstLinter, &ExternalAstRule)> {
|
||||
self.linters
|
||||
.iter()
|
||||
.filter(|linter| linter.enabled)
|
||||
.flat_map(|linter| linter.rules.iter().map(move |rule| (linter, rule)))
|
||||
}
|
||||
|
||||
pub fn iter_enabled_rule_locators(&self) -> impl Iterator<Item = RuleLocator> + '_ {
|
||||
self.linters
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, linter)| linter.enabled)
|
||||
.flat_map(|(linter_index, linter)| {
|
||||
linter
|
||||
.rules
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(rule_index, _)| RuleLocator::new(linter_index, rule_index))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expect_entry(&self, locator: RuleLocator) -> (&ExternalAstLinter, &ExternalAstRule) {
|
||||
let (rule, linter) = self
|
||||
.rule_entry(locator)
|
||||
.expect("rule locator does not reference a valid entry");
|
||||
(linter, rule)
|
||||
}
|
||||
}
|
||||
|
||||
impl ruff_cache::CacheKey for ExternalLintRegistry {
|
||||
|
|
|
|||
21
crates/ruff_linter/src/external/error.rs
vendored
21
crates/ruff_linter/src/external/error.rs
vendored
|
|
@ -75,4 +75,25 @@ pub enum ExternalLinterError {
|
|||
"external rule `{rule}` in linter `{linter}` declares `call-callee-regex` but does not target `expr:Call` nodes"
|
||||
)]
|
||||
CallCalleeRegexWithoutCallTarget { linter: String, rule: String },
|
||||
|
||||
#[error("{message}")]
|
||||
ScriptCompile { message: String },
|
||||
}
|
||||
|
||||
impl ExternalLinterError {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn format_script_compile_message(
|
||||
linter: &str,
|
||||
rule: &str,
|
||||
path: Option<PathBuf>,
|
||||
message: impl Into<String>,
|
||||
) -> String {
|
||||
let message = message.into();
|
||||
let location = path
|
||||
.map(|p| format!(" at {}", p.display()))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"failed to compile script for external rule `{rule}` in linter `{linter}`{location}: {message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ impl FromStr for RuleSelector {
|
|||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// **Changes should be reflected in `parse_no_redirect` as well**
|
||||
// External AST rules reserve the `EXT` prefix; short-circuit before we
|
||||
// attempt to interpret the selector as a built-in linter code.
|
||||
if s.starts_with("EXT") && ExternalRuleCode::new(s).is_ok() {
|
||||
return Ok(Self::External { code: s.into() });
|
||||
}
|
||||
match s {
|
||||
"ALL" => Ok(Self::All),
|
||||
"C" => Ok(Self::C),
|
||||
|
|
@ -134,7 +139,9 @@ pub enum ParseError {
|
|||
// TODO(martin): tell the user how to discover rule codes via the CLI once such a command is
|
||||
// implemented (but that should of course be done only in ruff and not here)
|
||||
Unknown(String),
|
||||
#[error("External rule selector `{0}` is not supported yet.")]
|
||||
#[error(
|
||||
"External rule selector `{0}` must be provided via `--select-external` or `lint.select-external`."
|
||||
)]
|
||||
External(String),
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +165,9 @@ impl Serialize for RuleSelector {
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
if let RuleSelector::External { code } = self {
|
||||
return serializer.serialize_str(code);
|
||||
}
|
||||
let (prefix, code) = self.prefix_and_code();
|
||||
serializer.serialize_str(&format!("{prefix}{code}"))
|
||||
}
|
||||
|
|
@ -196,7 +206,7 @@ impl Visitor<'_> for SelectorVisitor {
|
|||
Ok(value) => Ok(value),
|
||||
Err(err @ ParseError::External(_)) => Err(de::Error::custom(err.to_string())),
|
||||
Err(err) if looks_like_external_rule_code(v) => Err(de::Error::custom(format!(
|
||||
"{err}. External rule selectors are not supported yet."
|
||||
"{err}. External rule selectors must be provided via `lint.select-external`."
|
||||
))),
|
||||
Err(err) => Err(de::Error::custom(err)),
|
||||
}
|
||||
|
|
@ -384,6 +394,11 @@ impl RuleSelector {
|
|||
/// Parse [`RuleSelector`] from a string; but do not follow redirects.
|
||||
pub fn parse_no_redirect(s: &str) -> Result<Self, ParseError> {
|
||||
// **Changes should be reflected in `from_str` as well**
|
||||
// External AST rules reserve the `EXT` prefix; short-circuit before we
|
||||
// attempt to interpret the selector as a built-in linter code.
|
||||
if s.starts_with("EXT") && ExternalRuleCode::new(s).is_ok() {
|
||||
return Ok(Self::External { code: s.into() });
|
||||
}
|
||||
match s {
|
||||
"ALL" => Ok(Self::All),
|
||||
"C" => Ok(Self::C),
|
||||
|
|
@ -482,7 +497,9 @@ pub mod clap_completion {
|
|||
value.parse().map_err(|err| match err {
|
||||
ParseError::External(code) => clap::Error::raw(
|
||||
clap::error::ErrorKind::ValueValidation,
|
||||
format!("External rule selector `{code}` is not supported yet."),
|
||||
format!(
|
||||
"External rule selector `{code}` must be provided via `--select-external`."
|
||||
),
|
||||
),
|
||||
ParseError::Unknown(_) => {
|
||||
let mut error =
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use std::sync::LazyLock;
|
|||
use types::CompiledPerFileTargetVersionList;
|
||||
|
||||
use crate::codes::RuleCodePrefix;
|
||||
use crate::external::ExternalLintRegistry;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
|
|
@ -243,6 +244,9 @@ pub struct LinterSettings {
|
|||
pub builtins: Vec<String>,
|
||||
pub dummy_variable_rgx: Regex,
|
||||
pub external: Vec<String>,
|
||||
pub external_ast: Option<ExternalLintRegistry>,
|
||||
pub selected_external: Vec<String>,
|
||||
pub ignored_external: Vec<String>,
|
||||
pub ignore_init_module_imports: bool,
|
||||
pub logger_objects: Vec<String>,
|
||||
pub namespace_packages: Vec<PathBuf>,
|
||||
|
|
@ -319,6 +323,9 @@ impl Display for LinterSettings {
|
|||
self.typing_extensions,
|
||||
]
|
||||
}
|
||||
if let Some(registry) = &self.external_ast {
|
||||
writeln!(f, "linter.external-ast = {registry:#?}")?;
|
||||
}
|
||||
writeln!(f, "\n# Linter Plugins")?;
|
||||
display_settings! {
|
||||
formatter = f,
|
||||
|
|
@ -410,6 +417,9 @@ impl LinterSettings {
|
|||
dummy_variable_rgx: DUMMY_VARIABLE_RGX.clone(),
|
||||
|
||||
external: vec![],
|
||||
external_ast: None,
|
||||
selected_external: Vec::new(),
|
||||
ignored_external: Vec::new(),
|
||||
ignore_init_module_imports: true,
|
||||
logger_objects: vec![],
|
||||
namespace_packages: vec![],
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ use strum::IntoEnumIterator;
|
|||
use ruff_cache::cache_dir;
|
||||
use ruff_formatter::IndentStyle;
|
||||
use ruff_graph::{AnalyzeSettings, Direction, StringImports};
|
||||
use ruff_linter::external::{
|
||||
ExternalLintRegistry, PyprojectExternalLinterEntry, load_linter_into_registry,
|
||||
};
|
||||
use ruff_linter::line_width::{IndentWidth, LineLength};
|
||||
use ruff_linter::registry::{INCOMPATIBLE_CODES, Rule, RuleNamespace, RuleSet};
|
||||
use ruff_linter::rule_selector::{PreviewOptions, Specificity};
|
||||
|
|
@ -69,6 +72,14 @@ pub struct RuleSelection {
|
|||
pub extend_fixable: Vec<RuleSelector>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExternalRuleSelection {
|
||||
pub select: Option<Vec<String>>,
|
||||
pub extend_select: Vec<String>,
|
||||
pub ignore: Vec<String>,
|
||||
pub extend_ignore: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, is_macro::Is)]
|
||||
pub enum RuleSelectorKind {
|
||||
/// Enables the selected rules
|
||||
|
|
@ -112,6 +123,65 @@ impl RuleSelection {
|
|||
.map(|selector| (RuleSelectorKind::Modify, selector)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn selectors(&self) -> impl Iterator<Item = &RuleSelector> {
|
||||
self.selectors_by_kind().map(|(_, selector)| selector)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_external_registry(
|
||||
entries: &BTreeMap<String, ExternalLinterEntry>,
|
||||
) -> Result<Option<ExternalLintRegistry>> {
|
||||
let mut registry = ExternalLintRegistry::new();
|
||||
for (id, entry) in entries {
|
||||
if !entry.enabled {
|
||||
continue;
|
||||
}
|
||||
let py_entry = PyprojectExternalLinterEntry {
|
||||
toml_path: entry.path.clone(),
|
||||
enabled: entry.enabled,
|
||||
};
|
||||
load_linter_into_registry(&mut registry, id, &py_entry)?;
|
||||
}
|
||||
if registry.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(registry))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_external_rule_selections(
|
||||
selections: &[ExternalRuleSelection],
|
||||
) -> (Vec<String>, Vec<String>) {
|
||||
let mut selected: FxHashSet<String> = FxHashSet::default();
|
||||
let mut ignored: FxHashSet<String> = FxHashSet::default();
|
||||
|
||||
for selection in selections {
|
||||
if let Some(select) = &selection.select {
|
||||
selected = select.iter().cloned().collect();
|
||||
}
|
||||
for code in &selection.extend_select {
|
||||
selected.insert(code.clone());
|
||||
}
|
||||
for code in &selection.ignore {
|
||||
ignored.insert(code.clone());
|
||||
}
|
||||
for code in &selection.extend_ignore {
|
||||
ignored.insert(code.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut selected_vec: Vec<String> = selected.into_iter().collect();
|
||||
let mut ignored_vec: Vec<String> = ignored.into_iter().collect();
|
||||
selected_vec.sort_unstable();
|
||||
ignored_vec.sort_unstable();
|
||||
(selected_vec, ignored_vec)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternalLinterEntry {
|
||||
pub path: PathBuf,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
@ -239,7 +309,8 @@ impl Configuration {
|
|||
|
||||
let line_length = self.line_length.unwrap_or_default();
|
||||
|
||||
let rules = lint.as_rule_table(lint_preview)?;
|
||||
#[allow(unused_mut)]
|
||||
let mut rules = lint.as_rule_table(lint_preview)?;
|
||||
|
||||
// LinterSettings validation
|
||||
let isort = lint
|
||||
|
|
@ -257,6 +328,75 @@ impl Configuration {
|
|||
|
||||
let future_annotations = lint.future_annotations.unwrap_or_default();
|
||||
|
||||
let (configured_selected_external_vec, configured_ignored_external_vec) =
|
||||
resolve_external_rule_selections(&lint.external_rule_selections);
|
||||
|
||||
let mut external_codes = lint.external.unwrap_or_default();
|
||||
let mut seen_external_codes = external_codes.iter().cloned().collect::<FxHashSet<_>>();
|
||||
|
||||
let external_ast_registry = match lint.external_ast.as_ref() {
|
||||
Some(entries) => build_external_registry(entries)?,
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(registry) = external_ast_registry.as_ref() {
|
||||
for rule in registry.iter_enabled_rules() {
|
||||
let code = rule.code.as_str().to_owned();
|
||||
if seen_external_codes.insert(code.clone()) {
|
||||
external_codes.push(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
for code in &configured_selected_external_vec {
|
||||
if seen_external_codes.insert(code.clone()) {
|
||||
external_codes.push(code.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let available_external_codes = external_ast_registry.as_ref().map(|registry| {
|
||||
registry
|
||||
.iter_enabled_rules()
|
||||
.map(|rule| rule.code.as_str().to_string())
|
||||
.collect::<FxHashSet<_>>()
|
||||
});
|
||||
|
||||
let selectors = lint
|
||||
.rule_selections
|
||||
.iter()
|
||||
.flat_map(RuleSelection::selectors)
|
||||
.chain(lint.extend_safe_fixes.iter())
|
||||
.chain(lint.extend_unsafe_fixes.iter());
|
||||
|
||||
let mut missing_external = FxHashSet::default();
|
||||
for selector in selectors {
|
||||
if let RuleSelector::External { code } = selector {
|
||||
let is_known = available_external_codes
|
||||
.as_ref()
|
||||
.is_some_and(|available| available.contains(code.as_ref()));
|
||||
if !is_known {
|
||||
missing_external.insert(code.as_ref().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !missing_external.is_empty() {
|
||||
let mut missing: Vec<_> = missing_external.into_iter().collect();
|
||||
missing.sort_unstable();
|
||||
let formatted = missing
|
||||
.into_iter()
|
||||
.map(|code| format!("`{code}`"))
|
||||
.collect::<Vec<_>>();
|
||||
let message = if formatted.len() == 1 {
|
||||
format!("Unknown rule selector: {}", formatted[0])
|
||||
} else {
|
||||
format!("Unknown rule selectors: {}", formatted.join(", "))
|
||||
};
|
||||
return Err(anyhow!(message));
|
||||
}
|
||||
if external_ast_registry.is_none() {
|
||||
rules.disable(Rule::ExternalLinter);
|
||||
}
|
||||
|
||||
Ok(Settings {
|
||||
cache_dir: self
|
||||
.cache_dir
|
||||
|
|
@ -303,7 +443,10 @@ impl Configuration {
|
|||
dummy_variable_rgx: lint
|
||||
.dummy_variable_rgx
|
||||
.unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()),
|
||||
external: lint.external.unwrap_or_default(),
|
||||
external: external_codes,
|
||||
external_ast: external_ast_registry,
|
||||
selected_external: configured_selected_external_vec,
|
||||
ignored_external: configured_ignored_external_vec,
|
||||
ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or(true),
|
||||
line_length,
|
||||
tab_size: self.indent_width.unwrap_or_default(),
|
||||
|
|
@ -635,6 +778,7 @@ pub struct LintConfiguration {
|
|||
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
|
||||
pub rule_selections: Vec<RuleSelection>,
|
||||
pub explicit_preview_rules: Option<bool>,
|
||||
pub external_rule_selections: Vec<ExternalRuleSelection>,
|
||||
|
||||
// Fix configuration
|
||||
pub extend_unsafe_fixes: Vec<RuleSelector>,
|
||||
|
|
@ -644,6 +788,7 @@ pub struct LintConfiguration {
|
|||
pub allowed_confusables: Option<Vec<char>>,
|
||||
pub dummy_variable_rgx: Option<Regex>,
|
||||
pub external: Option<Vec<String>>,
|
||||
pub external_ast: Option<BTreeMap<String, ExternalLinterEntry>>,
|
||||
pub ignore_init_module_imports: Option<bool>,
|
||||
pub logger_objects: Option<Vec<String>>,
|
||||
pub task_tags: Option<Vec<String>>,
|
||||
|
|
@ -710,6 +855,118 @@ impl LintConfiguration {
|
|||
options.common.ignore_init_module_imports
|
||||
};
|
||||
|
||||
for (label, selectors) in [
|
||||
("lint.select", options.common.select.as_ref()),
|
||||
("lint.extend-select", options.common.extend_select.as_ref()),
|
||||
] {
|
||||
if let Some(selectors) = selectors {
|
||||
if let Some(code) = selectors.iter().find_map(|selector| {
|
||||
if let RuleSelector::External { code } = selector {
|
||||
Some(code.as_ref().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"External rule `{code}` cannot be enabled via `{label}`; use `lint.select-external` instead."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(internal) = options.common.select_external.as_ref().and_then(|values| {
|
||||
values.iter().find(|value| {
|
||||
matches!(
|
||||
RuleSelector::from_str(value),
|
||||
Ok(RuleSelector::Linter(_)
|
||||
| RuleSelector::Prefix { .. }
|
||||
| RuleSelector::Rule { .. })
|
||||
)
|
||||
})
|
||||
}) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Internal rule `{internal}` cannot be enabled via `lint.select-external`; use `lint.select` instead."
|
||||
));
|
||||
}
|
||||
if let Some(internal) = options
|
||||
.common
|
||||
.extend_select_external
|
||||
.as_ref()
|
||||
.and_then(|values| {
|
||||
values.iter().find(|value| {
|
||||
matches!(
|
||||
RuleSelector::from_str(value),
|
||||
Ok(RuleSelector::Linter(_)
|
||||
| RuleSelector::Prefix { .. }
|
||||
| RuleSelector::Rule { .. })
|
||||
)
|
||||
})
|
||||
})
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Internal rule `{internal}` cannot be enabled via `lint.extend-select-external`; use `lint.extend-select` instead."
|
||||
));
|
||||
}
|
||||
if let Some(internal) = options.common.ignore_external.as_ref().and_then(|values| {
|
||||
values.iter().find(|value| {
|
||||
matches!(
|
||||
RuleSelector::from_str(value),
|
||||
Ok(RuleSelector::Linter(_)
|
||||
| RuleSelector::Prefix { .. }
|
||||
| RuleSelector::Rule { .. })
|
||||
)
|
||||
})
|
||||
}) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Internal rule `{internal}` cannot be disabled via `lint.ignore-external`; use `lint.ignore` instead."
|
||||
));
|
||||
}
|
||||
if let Some(internal) = options
|
||||
.common
|
||||
.extend_ignore_external
|
||||
.as_ref()
|
||||
.and_then(|values| {
|
||||
values.iter().find(|value| {
|
||||
matches!(
|
||||
RuleSelector::from_str(value),
|
||||
Ok(RuleSelector::Linter(_)
|
||||
| RuleSelector::Prefix { .. }
|
||||
| RuleSelector::Rule { .. })
|
||||
)
|
||||
})
|
||||
})
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Internal rule `{internal}` cannot be disabled via `lint.extend-ignore-external`; use `lint.extend-ignore` instead."
|
||||
));
|
||||
}
|
||||
|
||||
let external_ast_entries = options
|
||||
.external_ast
|
||||
.map(|entries| {
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(id, entry)| {
|
||||
let raw_path = entry
|
||||
.path
|
||||
.ok_or_else(|| anyhow!("external linter `{id}` must define `path`"))?;
|
||||
let path = if raw_path.is_absolute() {
|
||||
raw_path
|
||||
} else {
|
||||
project_root.join(raw_path)
|
||||
};
|
||||
Ok((
|
||||
id,
|
||||
ExternalLinterEntry {
|
||||
path,
|
||||
enabled: entry.enabled.unwrap_or(true),
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<Result<BTreeMap<_, _>>>()
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(LintConfiguration {
|
||||
exclude: options.exclude.map(|paths| {
|
||||
paths
|
||||
|
|
@ -730,6 +987,12 @@ impl LintConfiguration {
|
|||
unfixable,
|
||||
extend_fixable: options.common.extend_fixable.unwrap_or_default(),
|
||||
}],
|
||||
external_rule_selections: vec![ExternalRuleSelection {
|
||||
select: options.common.select_external,
|
||||
extend_select: options.common.extend_select_external.unwrap_or_default(),
|
||||
ignore: options.common.ignore_external.unwrap_or_default(),
|
||||
extend_ignore: options.common.extend_ignore_external.unwrap_or_default(),
|
||||
}],
|
||||
extend_safe_fixes: options.common.extend_safe_fixes.unwrap_or_default(),
|
||||
extend_unsafe_fixes: options.common.extend_unsafe_fixes.unwrap_or_default(),
|
||||
allowed_confusables: options.common.allowed_confusables,
|
||||
|
|
@ -752,6 +1015,7 @@ impl LintConfiguration {
|
|||
})
|
||||
.unwrap_or_default(),
|
||||
external: options.common.external,
|
||||
external_ast: external_ast_entries,
|
||||
ignore_init_module_imports,
|
||||
explicit_preview_rules: options.common.explicit_preview_rules,
|
||||
per_file_ignores: options.common.per_file_ignores.map(|per_file_ignores| {
|
||||
|
|
@ -1133,6 +1397,8 @@ impl LintConfiguration {
|
|||
|
||||
let mut extend_per_file_ignores = config.extend_per_file_ignores;
|
||||
extend_per_file_ignores.extend(self.extend_per_file_ignores);
|
||||
let mut external_rule_selections = config.external_rule_selections;
|
||||
external_rule_selections.extend(self.external_rule_selections);
|
||||
|
||||
Self {
|
||||
exclude: self.exclude.or(config.exclude),
|
||||
|
|
@ -1140,10 +1406,12 @@ impl LintConfiguration {
|
|||
rule_selections,
|
||||
extend_safe_fixes,
|
||||
extend_unsafe_fixes,
|
||||
external_rule_selections,
|
||||
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
|
||||
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
|
||||
extend_per_file_ignores,
|
||||
external: self.external.or(config.external),
|
||||
external_ast: self.external_ast.or(config.external_ast),
|
||||
ignore_init_module_imports: self
|
||||
.ignore_init_module_imports
|
||||
.or(config.ignore_init_module_imports),
|
||||
|
|
@ -1408,6 +1676,8 @@ fn warn_about_deprecated_top_level_lint_options(
|
|||
pyupgrade,
|
||||
per_file_ignores,
|
||||
extend_per_file_ignores,
|
||||
select_external,
|
||||
..
|
||||
} = top_level_options;
|
||||
let mut used_options = Vec::new();
|
||||
|
||||
|
|
@ -1466,6 +1736,9 @@ fn warn_about_deprecated_top_level_lint_options(
|
|||
if select.is_some() {
|
||||
used_options.push("select");
|
||||
}
|
||||
if select_external.is_some() {
|
||||
used_options.push("select-external");
|
||||
}
|
||||
|
||||
if explicit_preview_rules.is_some() {
|
||||
used_options.push("explicit-preview-rules");
|
||||
|
|
|
|||
|
|
@ -554,6 +554,8 @@ pub struct LintOptions {
|
|||
"#
|
||||
)]
|
||||
pub future_annotations: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub external_ast: Option<BTreeMap<String, ExternalAstLinterOptions>>,
|
||||
}
|
||||
|
||||
/// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`].
|
||||
|
|
@ -569,6 +571,23 @@ impl OptionsMetadata for DeprecatedTopLevelLintOptions {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, OptionsMetadata, CombineOptions, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
pub struct ExternalAstLinterOptions {
|
||||
/// Path to the TOML file containing external lint rule definitions.
|
||||
#[option(
|
||||
default = "null",
|
||||
value_type = "path",
|
||||
example = r#"path = "lint/custom_rules.toml""#
|
||||
)]
|
||||
pub path: Option<PathBuf>,
|
||||
/// Whether this external linter should be considered during lint runs.
|
||||
#[option(default = "true", value_type = "bool", example = "enabled = false")]
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for DeprecatedTopLevelLintOptions {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
|
|
@ -666,6 +685,45 @@ pub struct LintCommonOptions {
|
|||
"#
|
||||
)]
|
||||
pub extend_select: Option<Vec<RuleSelector>>,
|
||||
/// A list of external linter IDs or rule codes to enable. When omitted, external linters remain disabled.
|
||||
#[option(
|
||||
default = "[]",
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
# Enable the `logging_interpolation` external linter by default.
|
||||
select-external = ["logging_interpolation"]
|
||||
"#
|
||||
)]
|
||||
pub select_external: Option<Vec<String>>,
|
||||
/// A list of external linter IDs or rule codes to enable, in addition to those specified by [`select-external`](#lint_select-external).
|
||||
#[option(
|
||||
default = "[]",
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
# Enable the `logging_interpolation` external linter alongside those specified by `select-external`.
|
||||
extend-select-external = ["logging_interpolation"]
|
||||
"#
|
||||
)]
|
||||
pub extend_select_external: Option<Vec<String>>,
|
||||
/// A list of external linter IDs or rule codes to ignore. Prefixes are not supported.
|
||||
#[option(
|
||||
default = "[]",
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
# Suppress the `logging_interpolation.dangerous` rule, even if the linter is otherwise enabled.
|
||||
ignore-external = ["logging_interpolation.dangerous"]
|
||||
"#
|
||||
)]
|
||||
pub ignore_external: Option<Vec<String>>,
|
||||
/// A list of external linter IDs or rule codes to ignore, in addition to those specified by [`ignore-external`](#lint_ignore-external).
|
||||
#[option(
|
||||
default = "[]",
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
extend-ignore-external = ["logging_interpolation.dangerous"]
|
||||
"#
|
||||
)]
|
||||
pub extend_ignore_external: Option<Vec<String>>,
|
||||
|
||||
/// A list of rule codes or prefixes to consider fixable, in addition to those
|
||||
/// specified by [`fixable`](#lint_fixable).
|
||||
|
|
@ -3904,9 +3962,14 @@ pub struct LintOptionsWire {
|
|||
dummy_variable_rgx: Option<String>,
|
||||
extend_ignore: Option<Vec<RuleSelector>>,
|
||||
extend_select: Option<Vec<RuleSelector>>,
|
||||
select_external: Option<Vec<String>>,
|
||||
extend_select_external: Option<Vec<String>>,
|
||||
ignore_external: Option<Vec<String>>,
|
||||
extend_ignore_external: Option<Vec<String>>,
|
||||
extend_fixable: Option<Vec<RuleSelector>>,
|
||||
extend_unfixable: Option<Vec<RuleSelector>>,
|
||||
external: Option<Vec<String>>,
|
||||
external_ast: Option<BTreeMap<String, ExternalAstLinterOptions>>,
|
||||
fixable: Option<Vec<RuleSelector>>,
|
||||
ignore: Option<Vec<RuleSelector>>,
|
||||
extend_safe_fixes: Option<Vec<RuleSelector>>,
|
||||
|
|
@ -3961,9 +4024,14 @@ impl From<LintOptionsWire> for LintOptions {
|
|||
dummy_variable_rgx,
|
||||
extend_ignore,
|
||||
extend_select,
|
||||
select_external,
|
||||
extend_select_external,
|
||||
ignore_external,
|
||||
extend_ignore_external,
|
||||
extend_fixable,
|
||||
extend_unfixable,
|
||||
external,
|
||||
external_ast,
|
||||
fixable,
|
||||
ignore,
|
||||
extend_safe_fixes,
|
||||
|
|
@ -4017,6 +4085,10 @@ impl From<LintOptionsWire> for LintOptions {
|
|||
dummy_variable_rgx,
|
||||
extend_ignore,
|
||||
extend_select,
|
||||
select_external,
|
||||
extend_select_external,
|
||||
ignore_external,
|
||||
extend_ignore_external,
|
||||
extend_fixable,
|
||||
extend_unfixable,
|
||||
external,
|
||||
|
|
@ -4064,6 +4136,7 @@ impl From<LintOptionsWire> for LintOptions {
|
|||
ruff,
|
||||
preview,
|
||||
typing_extensions,
|
||||
external_ast,
|
||||
future_annotations,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,23 @@ impl<'a> Resolver<'a> {
|
|||
pub fn settings(&self) -> impl Iterator<Item = &Settings> {
|
||||
std::iter::once(&self.pyproject_config.settings).chain(&self.settings)
|
||||
}
|
||||
|
||||
/// Return a mutable iterator over resolved [`Settings`] excluding the base configuration.
|
||||
pub fn settings_mut(&mut self) -> impl Iterator<Item = &mut Settings> {
|
||||
self.settings.iter_mut()
|
||||
}
|
||||
|
||||
/// Apply a transformation to each resolved [`Settings`] (excluding the base configuration)
|
||||
/// and return the [`Resolver`] for further use.
|
||||
pub fn transform_settings<F>(mut self, mut f: F) -> Result<Self>
|
||||
where
|
||||
F: FnMut(&mut Settings) -> Result<()>,
|
||||
{
|
||||
for settings in &mut self.settings {
|
||||
f(settings)?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around `detect_package_root` to cache filesystem lookups.
|
||||
|
|
|
|||
|
|
@ -678,6 +678,22 @@ Miscellaneous:
|
|||
Exit with a non-zero status code if any files were modified via fix,
|
||||
even if no lint violations remain
|
||||
|
||||
External linter options:
|
||||
--list-external-linters
|
||||
List configured external AST linters and exit
|
||||
--select-external <LINTER>
|
||||
Restrict linting to the given external linter IDs
|
||||
--extend-select-external <LINTER>
|
||||
Enable additional external linter IDs or rule codes without replacing
|
||||
existing selections
|
||||
--ignore-external <LINTER>
|
||||
Disable the given external linter IDs or rule codes
|
||||
--extend-ignore-external <LINTER>
|
||||
Disable additional external linter IDs or rule codes without
|
||||
replacing existing ignores
|
||||
--verify-external-linters
|
||||
Validate external linter definitions without running lint checks
|
||||
|
||||
Log levels:
|
||||
-v, --verbose Enable verbose logging
|
||||
-q, --quiet Print diagnostics, but nothing else
|
||||
|
|
|
|||
114
ruff.schema.json
generated
114
ruff.schema.json
generated
|
|
@ -109,6 +109,17 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"extend-ignore-external": {
|
||||
"description": "A list of external linter IDs or rule codes to ignore, in addition to those specified by [`ignore-external`](#lint_ignore-external).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"deprecated": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extend-include": {
|
||||
"description": "A list of file patterns to include when linting, in addition to those\nspecified by [`include`](#include).\n\nInclusion are based on globs, and should be single-path patterns, like\n`*.pyw`, to include any file with the `.pyw` extension.\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).",
|
||||
"type": [
|
||||
|
|
@ -155,6 +166,17 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"extend-select-external": {
|
||||
"description": "A list of external linter IDs or rule codes to enable, in addition to those specified by [`select-external`](#lint_select-external).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"deprecated": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extend-unfixable": {
|
||||
"description": "A list of rule codes or prefixes to consider non-auto-fixable, in addition to those\nspecified by [`unfixable`](#lint_unfixable).",
|
||||
"type": [
|
||||
|
|
@ -446,6 +468,17 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"ignore-external": {
|
||||
"description": "A list of external linter IDs or rule codes to ignore. Prefixes are not supported.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"deprecated": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ignore-init-module-imports": {
|
||||
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such\nimports will still be flagged, but with a dedicated message suggesting\nthat the import is either added to the module's `__all__` symbol, or\nre-exported with a redundant alias (e.g., `import os as os`).\n\nThis option is enabled by default, but you can opt-in to removal of imports\nvia an unsafe fix.",
|
||||
"type": [
|
||||
|
|
@ -684,6 +717,17 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"select-external": {
|
||||
"description": "A list of external linter IDs or rule codes to enable. When omitted, external linters remain disabled.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"deprecated": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"show-fixes": {
|
||||
"description": "Whether to show an enumeration of all fixed lint violations\n(overridden by the `--show-fixes` command-line flag).",
|
||||
"type": [
|
||||
|
|
@ -900,6 +944,27 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"ExternalAstLinterOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Whether this external linter should be considered during lint runs.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"path": {
|
||||
"description": "Path to the TOML file containing external lint rule definitions.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Flake8AnnotationsOptions": {
|
||||
"description": "Options for the `flake8-annotations` plugin.",
|
||||
"type": "object",
|
||||
|
|
@ -2009,6 +2074,16 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"extend-ignore-external": {
|
||||
"description": "A list of external linter IDs or rule codes to ignore, in addition to those specified by [`ignore-external`](#lint_ignore-external).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extend-per-file-ignores": {
|
||||
"description": "A list of mappings from file pattern to rule codes or prefixes to\nexclude, in addition to any rules excluded by [`per-file-ignores`](#lint_per-file-ignores).",
|
||||
"type": [
|
||||
|
|
@ -2042,6 +2117,16 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"extend-select-external": {
|
||||
"description": "A list of external linter IDs or rule codes to enable, in addition to those specified by [`select-external`](#lint_select-external).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extend-unfixable": {
|
||||
"description": "A list of rule codes or prefixes to consider non-auto-fixable, in addition to those\nspecified by [`unfixable`](#lint_unfixable).",
|
||||
"type": [
|
||||
|
|
@ -2073,6 +2158,15 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"external-ast": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/ExternalAstLinterOptions"
|
||||
}
|
||||
},
|
||||
"fixable": {
|
||||
"description": "A list of rule codes or prefixes to consider fixable. By default,\nall rules are considered fixable.",
|
||||
"type": [
|
||||
|
|
@ -2287,6 +2381,16 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"ignore-external": {
|
||||
"description": "A list of external linter IDs or rule codes to ignore. Prefixes are not supported.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ignore-init-module-imports": {
|
||||
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such\nimports will still be flagged, but with a dedicated message suggesting\nthat the import is either added to the module's `__all__` symbol, or\nre-exported with a redundant alias (e.g., `import os as os`).\n\nThis option is enabled by default, but you can opt-in to removal of imports\nvia an unsafe fix.",
|
||||
"type": [
|
||||
|
|
@ -2445,6 +2549,16 @@
|
|||
"$ref": "#/definitions/RuleSelector"
|
||||
}
|
||||
},
|
||||
"select-external": {
|
||||
"description": "A list of external linter IDs or rule codes to enable. When omitted, external linters remain disabled.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"task-tags": {
|
||||
"description": "A list of task tags to recognize (e.g., \"TODO\", \"FIXME\", \"XXX\").\n\nComments starting with these tags will be ignored by commented-out code\ndetection (`ERA`), and skipped by line-length rules (`E501`) if\n[`ignore-overlong-task-comments`](#lint_pycodestyle_ignore-overlong-task-comments) is set to `true`.",
|
||||
"type": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue