Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
642 changes: 335 additions & 307 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions crates/ruff/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,10 @@ tikv-jemallocator = { workspace = true }

[lints]
workspace = true

[features]
default = []
ext-lint = [
"ruff_linter/ext-lint",
"ruff_workspace/ext-lint",
]
163 changes: 162 additions & 1 deletion crates/ruff/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -469,6 +469,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)]
Expand Down Expand Up @@ -666,6 +713,14 @@ impl ConfigArguments {
self.config_file.as_deref()
}

pub(crate) fn has_cli_external_selection(&self) -> bool {
self.per_flag_overrides
.select_external
.as_ref()
.is_some_and(|selection| !selection.is_empty())
|| !self.per_flag_overrides.extend_select_external.is_empty()
}

fn from_cli_arguments(
global_options: GlobalConfigArgs,
per_flag_overrides: ExplicitConfigOverrides,
Expand Down Expand Up @@ -737,6 +792,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,
Expand All @@ -748,6 +867,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,
Expand Down Expand Up @@ -781,6 +906,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()
};

Expand Down Expand Up @@ -1083,6 +1212,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,
Expand Down Expand Up @@ -1347,6 +1482,10 @@ struct ExplicitConfigOverrides {
detect_string_imports: Option<bool>,
string_imports_min_dots: Option<usize>,
type_checking_imports: Option<bool>,
select_external: Option<Vec<String>>,
extend_select_external: Vec<String>,
ignore_external: Vec<String>,
extend_ignore_external: Vec<String>,
}

impl ConfigurationTransformer for ExplicitConfigOverrides {
Expand Down Expand Up @@ -1440,11 +1579,33 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
if let Some(type_checking_imports) = &self.type_checking_imports {
config.analyze.type_checking_imports = Some(*type_checking_imports);
}
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();
Expand Down
29 changes: 29 additions & 0 deletions crates/ruff/src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ 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,
};

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(
Expand All @@ -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());

Expand All @@ -49,6 +65,19 @@ pub(crate) fn check(
return Ok(Diagnostics::default());
}

let resolver = resolver.transform_settings(apply_external_selection)?;
let selection_from_cli = config_arguments.has_cli_external_selection();
let any_external_selection = selection_from_cli
|| 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
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff/src/commands/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&notebook_index);
Expand Down
Loading