mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 03:36:18 +00:00
Merge 3c786d5fb2 into 0d2cd84df4
This commit is contained in:
commit
d3333163b3
18 changed files with 1510 additions and 24 deletions
|
|
@ -775,7 +775,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`
|
||||
Unknown rule selector: `F481`. External rule selectors are not supported yet.
|
||||
|
||||
For more information, try '--help'.
|
||||
");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use std::path::Path;
|
|||
use itertools::Itertools;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_db::diagnostic::SecondaryCode;
|
||||
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
|
|
@ -77,7 +79,7 @@ pub(crate) fn check_noqa(
|
|||
{
|
||||
let suppressed = match &directive_line.directive {
|
||||
Directive::All(_) => {
|
||||
let Ok(rule) = Rule::from_code(code) else {
|
||||
let Some(rule) = resolve_rule_for_noqa(code, settings) else {
|
||||
debug_assert!(false, "Invalid secondary code `{code}`");
|
||||
continue;
|
||||
};
|
||||
|
|
@ -87,7 +89,7 @@ pub(crate) fn check_noqa(
|
|||
}
|
||||
Directive::Codes(directive) => {
|
||||
if directive.includes(code) {
|
||||
let Ok(rule) = Rule::from_code(code) else {
|
||||
let Some(rule) = resolve_rule_for_noqa(code, settings) else {
|
||||
debug_assert!(false, "Invalid secondary code `{code}`");
|
||||
continue;
|
||||
};
|
||||
|
|
@ -258,3 +260,19 @@ pub(crate) fn check_noqa(
|
|||
ignored_diagnostics.sort_unstable();
|
||||
ignored_diagnostics
|
||||
}
|
||||
|
||||
fn resolve_rule_for_noqa(code: &SecondaryCode, settings: &LinterSettings) -> Option<Rule> {
|
||||
if let Ok(rule) = Rule::from_code(code.as_str()) {
|
||||
return Some(rule);
|
||||
}
|
||||
|
||||
if settings
|
||||
.external
|
||||
.iter()
|
||||
.any(|external| code.as_str().starts_with(external))
|
||||
{
|
||||
return Some(Rule::ExternalLinter);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1064,6 +1064,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Ruff, "102") => rules::ruff::rules::InvalidRuleCode,
|
||||
|
||||
(Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml,
|
||||
(Ruff, "300") => rules::ruff::rules::ExternalLinter,
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
(Ruff, "900") => rules::ruff::rules::StableTestRule,
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
|
|
@ -1091,7 +1092,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
#[cfg(any(feature = "test-rules", test))]
|
||||
(Ruff, "990") => rules::ruff::rules::PanicyTestRule,
|
||||
|
||||
|
||||
// flake8-django
|
||||
(Flake8Django, "001") => rules::flake8_django::rules::DjangoNullableModelStringField,
|
||||
(Flake8Django, "003") => rules::flake8_django::rules::DjangoLocalsInRenderFunction,
|
||||
|
|
|
|||
26
crates/ruff_linter/src/external/ast/definition.rs
vendored
Normal file
26
crates/ruff_linter/src/external/ast/definition.rs
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::external::ast::rule::ExternalAstRuleSpec;
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ExternalAstLinterFile {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "rule")]
|
||||
pub rules: Vec<ExternalAstRuleSpec>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn _assert_specs_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<ExternalAstRuleSpec>();
|
||||
}
|
||||
420
crates/ruff_linter/src/external/ast/loader.rs
vendored
Normal file
420
crates/ruff_linter/src/external/ast/loader.rs
vendored
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::external::PyprojectExternalLinterEntry;
|
||||
use crate::external::ast::definition::ExternalAstLinterFile;
|
||||
use crate::external::ast::registry::ExternalLintRegistry;
|
||||
use crate::external::ast::rule::{
|
||||
CallCalleeMatcher, ExternalAstLinter, ExternalAstRule, ExternalAstRuleSpec, ExternalRuleCode,
|
||||
ExternalRuleCodeError, ExternalRuleScript,
|
||||
};
|
||||
use crate::external::ast::target::{AstTarget, AstTargetSpec, ExprKind};
|
||||
use crate::external::error::ExternalLinterError;
|
||||
|
||||
pub fn load_linter_into_registry(
|
||||
registry: &mut ExternalLintRegistry,
|
||||
id: &str,
|
||||
entry: &PyprojectExternalLinterEntry,
|
||||
) -> Result<(), ExternalLinterError> {
|
||||
let linter = load_linter_from_entry(id, entry)?;
|
||||
registry.insert_linter(linter)
|
||||
}
|
||||
|
||||
pub fn load_linter_from_entry(
|
||||
id: &str,
|
||||
entry: &PyprojectExternalLinterEntry,
|
||||
) -> Result<ExternalAstLinter, ExternalLinterError> {
|
||||
let definition = load_definition_file(&entry.toml_path)?;
|
||||
build_linter(id, entry, &definition)
|
||||
}
|
||||
|
||||
fn load_definition_file(path: &Path) -> Result<ExternalAstLinterFile, ExternalLinterError> {
|
||||
let contents = fs::read_to_string(path).map_err(|source| ExternalLinterError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
toml::from_str(&contents).map_err(|source| ExternalLinterError::Parse {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_linter(
|
||||
id: &str,
|
||||
entry: &PyprojectExternalLinterEntry,
|
||||
linter_file: &ExternalAstLinterFile,
|
||||
) -> Result<ExternalAstLinter, ExternalLinterError> {
|
||||
if linter_file.rules.is_empty() {
|
||||
return Err(ExternalLinterError::EmptyLinter { id: id.to_string() });
|
||||
}
|
||||
|
||||
let resolved_dir = entry
|
||||
.toml_path
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
|
||||
let mut codes = HashSet::new();
|
||||
let mut rules = Vec::with_capacity(linter_file.rules.len());
|
||||
|
||||
for rule_spec in &linter_file.rules {
|
||||
let rule = build_rule(id, &resolved_dir, rule_spec)?;
|
||||
|
||||
if !codes.insert(rule.code.as_str().to_string()) {
|
||||
return Err(ExternalLinterError::DuplicateRule {
|
||||
linter: id.to_string(),
|
||||
code: rule.code.as_str().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
rules.push(rule);
|
||||
}
|
||||
|
||||
let linter = ExternalAstLinter::new(
|
||||
id,
|
||||
linter_file.name.clone().unwrap_or_else(|| id.to_string()),
|
||||
linter_file.description.clone(),
|
||||
entry.enabled && linter_file.enabled,
|
||||
rules,
|
||||
);
|
||||
|
||||
Ok(linter)
|
||||
}
|
||||
|
||||
fn build_rule(
|
||||
linter_id: &str,
|
||||
base_dir: &Path,
|
||||
spec: &ExternalAstRuleSpec,
|
||||
) -> Result<ExternalAstRule, ExternalLinterError> {
|
||||
let code = ExternalRuleCode::new(&spec.code).map_err(|error| match error {
|
||||
ExternalRuleCodeError::Empty | ExternalRuleCodeError::InvalidCharacters(_) => {
|
||||
ExternalLinterError::InvalidRuleCode {
|
||||
linter: linter_id.to_string(),
|
||||
code: spec.code.clone(),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
if spec.targets.is_empty() {
|
||||
return Err(ExternalLinterError::MissingTargets {
|
||||
linter: linter_id.to_string(),
|
||||
rule: spec.name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut resolved_targets = Vec::with_capacity(spec.targets.len());
|
||||
for target in &spec.targets {
|
||||
let parsed = parse_target(target).map_err(|source| ExternalLinterError::UnknownTarget {
|
||||
linter: linter_id.to_string(),
|
||||
rule: spec.name.clone(),
|
||||
target: target.raw().to_string(),
|
||||
source,
|
||||
})?;
|
||||
resolved_targets.push(parsed);
|
||||
}
|
||||
|
||||
let script = resolve_script(linter_id, &spec.name, base_dir, &spec.script)?;
|
||||
let call_callee = if let Some(pattern) = spec.call_callee_regex.as_ref() {
|
||||
if !resolved_targets
|
||||
.iter()
|
||||
.any(|target| matches!(target, AstTarget::Expr(ExprKind::Call)))
|
||||
{
|
||||
return Err(ExternalLinterError::CallCalleeRegexWithoutCallTarget {
|
||||
linter: linter_id.to_string(),
|
||||
rule: spec.name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Some(CallCalleeMatcher::new(pattern.clone()).map_err(|source| {
|
||||
ExternalLinterError::InvalidCallCalleeRegex {
|
||||
linter: linter_id.to_string(),
|
||||
rule: spec.name.clone(),
|
||||
pattern: pattern.clone(),
|
||||
source,
|
||||
}
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ExternalAstRule::new(
|
||||
code,
|
||||
spec.name.clone(),
|
||||
spec.summary.clone(),
|
||||
resolved_targets,
|
||||
script,
|
||||
call_callee,
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_script(
|
||||
linter_id: &str,
|
||||
rule_name: &str,
|
||||
base_dir: &Path,
|
||||
script_path: &Path,
|
||||
) -> Result<ExternalRuleScript, ExternalLinterError> {
|
||||
let resolved = if script_path.is_absolute() {
|
||||
script_path.to_path_buf()
|
||||
} else {
|
||||
base_dir.join(script_path)
|
||||
};
|
||||
let contents =
|
||||
fs::read_to_string(&resolved).map_err(|source| ExternalLinterError::ScriptIo {
|
||||
linter: linter_id.to_string(),
|
||||
rule: rule_name.to_string(),
|
||||
path: resolved.clone(),
|
||||
source,
|
||||
})?;
|
||||
if contents.trim().is_empty() {
|
||||
return Err(ExternalLinterError::MissingScriptBody {
|
||||
linter: linter_id.to_string(),
|
||||
rule: rule_name.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(ExternalRuleScript::file(resolved, contents))
|
||||
}
|
||||
|
||||
fn parse_target(
|
||||
spec: &AstTargetSpec,
|
||||
) -> Result<AstTarget, crate::external::ast::target::AstTargetParseError> {
|
||||
spec.parse()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::external::ast::target::{ExprKind, StmtKind};
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn write(path: &Path, contents: &str) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(path, contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_linter_from_entry_resolves_relative_paths() -> Result<()> {
|
||||
let temp = tempdir()?;
|
||||
let linter_path = temp.path().join("linters/my_linter.toml");
|
||||
let script_path = temp.path().join("linters/rules/example.py");
|
||||
let call_script_path = temp.path().join("linters/rules/call.py");
|
||||
|
||||
write(
|
||||
&script_path,
|
||||
r#"
|
||||
def check():
|
||||
# placeholder body
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
|
||||
write(
|
||||
&call_script_path,
|
||||
r#"
|
||||
def check():
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
|
||||
write(
|
||||
&linter_path,
|
||||
r#"
|
||||
name = "Example External Linter"
|
||||
description = "Demonstrates external AST configuration"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT001"
|
||||
name = "ExampleRule"
|
||||
summary = "Flags demo targets"
|
||||
targets = ["stmt:FunctionDef"]
|
||||
script = "rules/example.py"
|
||||
|
||||
[[rule]]
|
||||
code = "EXT100"
|
||||
name = "CallRule"
|
||||
targets = ["expr:Call"]
|
||||
call-callee-regex = "^logging\\."
|
||||
script = "rules/call.py"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let entry = PyprojectExternalLinterEntry {
|
||||
toml_path: linter_path,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
let linter = load_linter_from_entry("example", &entry)?;
|
||||
assert!(linter.enabled);
|
||||
assert_eq!(linter.id.as_str(), "example");
|
||||
assert_eq!(linter.name.as_str(), "Example External Linter");
|
||||
assert_eq!(
|
||||
linter.description.as_deref(),
|
||||
Some("Demonstrates external AST configuration")
|
||||
);
|
||||
assert_eq!(linter.rules.len(), 2);
|
||||
|
||||
let example_rule = &linter.rules[0];
|
||||
assert_eq!(example_rule.code.as_str(), "EXT001");
|
||||
assert_eq!(example_rule.name.as_str(), "ExampleRule");
|
||||
assert_eq!(example_rule.summary.as_deref(), Some("Flags demo targets"));
|
||||
assert_eq!(example_rule.targets.len(), 1);
|
||||
assert_eq!(
|
||||
example_rule.targets[0],
|
||||
AstTarget::Stmt(StmtKind::FunctionDef)
|
||||
);
|
||||
assert_eq!(example_rule.script.path(), script_path.as_path());
|
||||
assert!(example_rule.script.body().contains("placeholder body"));
|
||||
|
||||
let call_rule = &linter.rules[1];
|
||||
assert_eq!(call_rule.code.as_str(), "EXT100");
|
||||
assert_eq!(call_rule.name.as_str(), "CallRule");
|
||||
assert_eq!(call_rule.targets[0], AstTarget::Expr(ExprKind::Call));
|
||||
let call_callee = call_rule
|
||||
.call_callee()
|
||||
.expect("expected call callee matcher to be present");
|
||||
assert_eq!(call_callee.pattern(), "^logging\\.");
|
||||
assert!(call_callee.regex().is_match("logging.info"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_linter_rejects_call_regex_without_call_target() -> Result<()> {
|
||||
let temp = tempdir()?;
|
||||
let linter_path = temp.path().join("linters/invalid-call.toml");
|
||||
let script_path = temp.path().join("linters/rules/invalid.py");
|
||||
|
||||
write(
|
||||
&script_path,
|
||||
r#"
|
||||
def check():
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
|
||||
write(
|
||||
&linter_path,
|
||||
r#"
|
||||
[[rule]]
|
||||
code = "EXT101"
|
||||
name = "InvalidCallRule"
|
||||
targets = ["expr:Name"]
|
||||
call-callee-regex = "^logging\\."
|
||||
script = "rules/invalid.py"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let entry = PyprojectExternalLinterEntry {
|
||||
toml_path: linter_path,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
let err = load_linter_from_entry("invalid-call", &entry).unwrap_err();
|
||||
let ExternalLinterError::CallCalleeRegexWithoutCallTarget { linter, rule } = err else {
|
||||
panic!("expected call regex without target error");
|
||||
};
|
||||
assert_eq!(linter, "invalid-call");
|
||||
assert_eq!(rule, "InvalidCallRule");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_linter_rejects_invalid_call_regex() -> Result<()> {
|
||||
let temp = tempdir()?;
|
||||
let linter_path = temp.path().join("linters/bad-regex.toml");
|
||||
let script_path = temp.path().join("linters/rules/bad.py");
|
||||
|
||||
write(
|
||||
&script_path,
|
||||
r#"
|
||||
def check():
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
|
||||
write(
|
||||
&linter_path,
|
||||
r#"
|
||||
[[rule]]
|
||||
code = "EXT102"
|
||||
name = "BadRegexRule"
|
||||
targets = ["expr:Call"]
|
||||
call-callee-regex = "["
|
||||
script = "rules/bad.py"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let entry = PyprojectExternalLinterEntry {
|
||||
toml_path: linter_path,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
let err = load_linter_from_entry("bad-regex", &entry).unwrap_err();
|
||||
let ExternalLinterError::InvalidCallCalleeRegex {
|
||||
linter,
|
||||
rule,
|
||||
pattern,
|
||||
..
|
||||
} = err
|
||||
else {
|
||||
panic!("expected invalid call regex error");
|
||||
};
|
||||
assert_eq!(linter, "bad-regex");
|
||||
assert_eq!(rule, "BadRegexRule");
|
||||
assert_eq!(pattern, "[");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_linter_into_registry_marks_disabled_linters() -> Result<()> {
|
||||
let temp = tempdir()?;
|
||||
let linter_path = temp.path().join("linters/disabled.toml");
|
||||
let script_path = temp.path().join("linters/rules/unused.py");
|
||||
|
||||
write(
|
||||
&script_path,
|
||||
r#"
|
||||
def check():
|
||||
pass
|
||||
"#,
|
||||
)?;
|
||||
|
||||
write(
|
||||
&linter_path,
|
||||
r#"
|
||||
enabled = false
|
||||
|
||||
[[rule]]
|
||||
code = "EXT002"
|
||||
name = "DisabledRule"
|
||||
targets = ["stmt:Expr"]
|
||||
script = "rules/unused.py"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let entry = PyprojectExternalLinterEntry {
|
||||
toml_path: linter_path,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
let mut registry = ExternalLintRegistry::new();
|
||||
load_linter_into_registry(&mut registry, "disabled", &entry)?;
|
||||
|
||||
assert_eq!(registry.linters().len(), 1);
|
||||
|
||||
let linter = ®istry.linters()[0];
|
||||
assert!(!linter.enabled);
|
||||
|
||||
// Disabled linters should not be discoverable by rule code lookup.
|
||||
assert!(registry.find_rule_by_code("EXT002").is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
6
crates/ruff_linter/src/external/ast/mod.rs
vendored
Normal file
6
crates/ruff_linter/src/external/ast/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod definition;
|
||||
pub mod loader;
|
||||
pub mod registry;
|
||||
pub mod rule;
|
||||
pub mod runtime;
|
||||
pub mod target;
|
||||
163
crates/ruff_linter/src/external/ast/registry.rs
vendored
Normal file
163
crates/ruff_linter/src/external/ast/registry.rs
vendored
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
use std::hash::Hasher;
|
||||
|
||||
use crate::external::ast::rule::{CallCalleeMatcher, ExternalAstLinter, ExternalAstRule};
|
||||
use crate::external::ast::target::{AstTarget, ExprKind, StmtKind};
|
||||
use crate::external::error::ExternalLinterError;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct RuleLocator {
|
||||
pub linter_index: usize,
|
||||
pub rule_index: usize,
|
||||
}
|
||||
|
||||
impl RuleLocator {
|
||||
pub const fn new(linter_index: usize, rule_index: usize) -> Self {
|
||||
Self {
|
||||
linter_index,
|
||||
rule_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ExternalLintRegistry {
|
||||
linters: Vec<ExternalAstLinter>,
|
||||
index_by_code: FxHashMap<String, RuleLocator>,
|
||||
stmt_index: FxHashMap<StmtKind, Vec<RuleLocator>>,
|
||||
expr_index: FxHashMap<ExprKind, Vec<RuleLocator>>,
|
||||
}
|
||||
|
||||
impl ExternalLintRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.linters.is_empty()
|
||||
}
|
||||
|
||||
pub fn linters(&self) -> &[ExternalAstLinter] {
|
||||
&self.linters
|
||||
}
|
||||
|
||||
pub fn insert_linter(&mut self, linter: ExternalAstLinter) -> Result<(), ExternalLinterError> {
|
||||
if self.linters.iter().any(|existing| existing.id == linter.id) {
|
||||
return Err(ExternalLinterError::DuplicateLinter { id: linter.id });
|
||||
}
|
||||
|
||||
let linter_index = self.linters.len();
|
||||
for (rule_index, rule) in linter.rules.iter().enumerate() {
|
||||
let code = rule.code.as_str().to_string();
|
||||
if self.index_by_code.contains_key(&code) {
|
||||
return Err(ExternalLinterError::DuplicateRule {
|
||||
linter: linter.id.clone(),
|
||||
code,
|
||||
});
|
||||
}
|
||||
self.index_by_code
|
||||
.insert(code, RuleLocator::new(linter_index, rule_index));
|
||||
|
||||
if !linter.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
for target in &rule.targets {
|
||||
match target {
|
||||
AstTarget::Stmt(kind) => self
|
||||
.stmt_index
|
||||
.entry(*kind)
|
||||
.or_default()
|
||||
.push(RuleLocator::new(linter_index, rule_index)),
|
||||
AstTarget::Expr(kind) => self
|
||||
.expr_index
|
||||
.entry(*kind)
|
||||
.or_default()
|
||||
.push(RuleLocator::new(linter_index, rule_index)),
|
||||
}
|
||||
}
|
||||
}
|
||||
self.linters.push(linter);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_rule(&self, locator: RuleLocator) -> Option<&ExternalAstRule> {
|
||||
self.linters
|
||||
.get(locator.linter_index)
|
||||
.and_then(|linter| linter.rules.get(locator.rule_index))
|
||||
}
|
||||
|
||||
pub fn get_linter(&self, locator: RuleLocator) -> Option<&ExternalAstLinter> {
|
||||
self.linters.get(locator.linter_index)
|
||||
}
|
||||
|
||||
pub fn find_rule_by_code(
|
||||
&self,
|
||||
code: &str,
|
||||
) -> Option<(RuleLocator, &ExternalAstRule, &ExternalAstLinter)> {
|
||||
let locator = *self.index_by_code.get(code)?;
|
||||
let linter = self.linters.get(locator.linter_index)?;
|
||||
if !linter.enabled {
|
||||
return None;
|
||||
}
|
||||
let rule = linter.rules.get(locator.rule_index)?;
|
||||
Some((locator, rule, linter))
|
||||
}
|
||||
|
||||
pub fn rules_for_stmt(&self, kind: StmtKind) -> impl Iterator<Item = RuleLocator> + '_ {
|
||||
self.stmt_index.get(&kind).into_iter().flatten().copied()
|
||||
}
|
||||
|
||||
pub fn rules_for_expr(&self, kind: ExprKind) -> impl Iterator<Item = RuleLocator> + '_ {
|
||||
self.expr_index.get(&kind).into_iter().flatten().copied()
|
||||
}
|
||||
|
||||
pub fn rule_entry(
|
||||
&self,
|
||||
locator: RuleLocator,
|
||||
) -> Option<(&ExternalAstRule, &ExternalAstLinter)> {
|
||||
let linter = self.linters.get(locator.linter_index)?;
|
||||
let rule = linter.rules.get(locator.rule_index)?;
|
||||
Some((rule, linter))
|
||||
}
|
||||
}
|
||||
|
||||
impl ruff_cache::CacheKey for ExternalLintRegistry {
|
||||
fn cache_key(&self, key: &mut ruff_cache::CacheKeyHasher) {
|
||||
key.write_usize(self.linters.len());
|
||||
for linter in &self.linters {
|
||||
linter.id.as_str().cache_key(key);
|
||||
linter.enabled.cache_key(key);
|
||||
linter.name.as_str().cache_key(key);
|
||||
linter.description.as_deref().cache_key(key);
|
||||
key.write_usize(linter.rules.len());
|
||||
for rule in &linter.rules {
|
||||
rule.code.as_str().cache_key(key);
|
||||
rule.name.as_str().cache_key(key);
|
||||
rule.summary.as_deref().cache_key(key);
|
||||
rule.call_callee()
|
||||
.map(CallCalleeMatcher::pattern)
|
||||
.cache_key(key);
|
||||
key.write_usize(rule.targets.len());
|
||||
for target in &rule.targets {
|
||||
match target {
|
||||
AstTarget::Stmt(kind) => {
|
||||
key.write_u8(0);
|
||||
key.write_u16(*kind as u16);
|
||||
}
|
||||
AstTarget::Expr(kind) => {
|
||||
key.write_u8(1);
|
||||
key.write_u16(*kind as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
let path_str = rule.script.path().to_string_lossy();
|
||||
key.write_usize(path_str.len());
|
||||
key.write(path_str.as_bytes());
|
||||
let contents_str = rule.script.body();
|
||||
key.write_usize(contents_str.len());
|
||||
key.write(contents_str.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
203
crates/ruff_linter/src/external/ast/rule.rs
vendored
Normal file
203
crates/ruff_linter/src/external/ast/rule.rs
vendored
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex::Regex;
|
||||
use ruff_db::diagnostic::SecondaryCode;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::external::ast::target::{AstTarget, AstTargetSpec};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExternalRuleCode(Box<str>);
|
||||
|
||||
impl ExternalRuleCode {
|
||||
pub fn new<S: AsRef<str>>(code: S) -> Result<Self, ExternalRuleCodeError> {
|
||||
let code_ref = code.as_ref();
|
||||
if code_ref.is_empty() {
|
||||
return Err(ExternalRuleCodeError::Empty);
|
||||
}
|
||||
if !Self::matches_format(code_ref) {
|
||||
return Err(ExternalRuleCodeError::InvalidCharacters(
|
||||
code_ref.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(Self(code_ref.into()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_secondary_code(&self) -> SecondaryCode {
|
||||
SecondaryCode::new(self.as_str().to_string())
|
||||
}
|
||||
|
||||
fn pattern() -> &'static Regex {
|
||||
static PATTERN: OnceLock<Regex> = OnceLock::new();
|
||||
PATTERN.get_or_init(|| Regex::new(r"^[A-Z]+[0-9]+$").expect("valid external rule regex"))
|
||||
}
|
||||
|
||||
pub(crate) fn matches_format(code: &str) -> bool {
|
||||
Self::pattern().is_match(code)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExternalRuleCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExternalRuleCodeError {
|
||||
#[error("external rule codes must not be empty")]
|
||||
Empty,
|
||||
#[error("external rule codes must contain only uppercase ASCII letters and digits: `{0}`")]
|
||||
InvalidCharacters(String),
|
||||
}
|
||||
|
||||
/// Fully resolved script content that can be handed to the runtime for compilation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternalRuleScript {
|
||||
path: PathBuf,
|
||||
contents: String,
|
||||
}
|
||||
|
||||
impl ExternalRuleScript {
|
||||
pub fn file(path: PathBuf, contents: impl Into<String>) -> Self {
|
||||
Self {
|
||||
path,
|
||||
contents: contents.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn body(&self) -> &str {
|
||||
&self.contents
|
||||
}
|
||||
}
|
||||
|
||||
/// User-facing metadata describing an external AST rule before targets are resolved.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ExternalAstRuleSpec {
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default)]
|
||||
pub targets: Vec<AstTargetSpec>,
|
||||
#[serde(default, rename = "call-callee-regex")]
|
||||
pub call_callee_regex: Option<String>,
|
||||
pub script: PathBuf,
|
||||
}
|
||||
|
||||
/// A validated, ready-to-run external AST rule definition.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternalAstRule {
|
||||
pub code: ExternalRuleCode,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub targets: Box<[AstTarget]>,
|
||||
pub script: ExternalRuleScript,
|
||||
pub call_callee: Option<CallCalleeMatcher>,
|
||||
}
|
||||
|
||||
impl ExternalAstRule {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
code: ExternalRuleCode,
|
||||
name: impl Into<String>,
|
||||
summary: Option<impl Into<String>>,
|
||||
targets: Vec<AstTarget>,
|
||||
script: ExternalRuleScript,
|
||||
call_callee: Option<CallCalleeMatcher>,
|
||||
) -> Self {
|
||||
let targets = targets.into_boxed_slice();
|
||||
Self {
|
||||
code,
|
||||
name: name.into(),
|
||||
summary: summary.map(Into::into),
|
||||
targets,
|
||||
script,
|
||||
call_callee,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_callee(&self) -> Option<&CallCalleeMatcher> {
|
||||
self.call_callee.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata about a collection of external AST rules loaded from a user-defined linter file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternalAstLinter {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub enabled: bool,
|
||||
pub rules: Vec<ExternalAstRule>,
|
||||
}
|
||||
|
||||
impl ExternalAstLinter {
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
description: Option<impl Into<String>>,
|
||||
enabled: bool,
|
||||
rules: Vec<ExternalAstRule>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
description: description.map(Into::into),
|
||||
enabled,
|
||||
rules,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExternalAstLinter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(
|
||||
f,
|
||||
"{}{}",
|
||||
self.id,
|
||||
if self.enabled { "" } else { " (disabled)" }
|
||||
)?;
|
||||
writeln!(f, " name: {}", self.name)?;
|
||||
if let Some(description) = &self.description {
|
||||
writeln!(f, " description: {description}")?;
|
||||
}
|
||||
writeln!(f, " rules:")?;
|
||||
for rule in &self.rules {
|
||||
writeln!(f, " - {} ({})", rule.code.as_str(), rule.name)?;
|
||||
}
|
||||
writeln!(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CallCalleeMatcher {
|
||||
pattern: String,
|
||||
regex: Regex,
|
||||
}
|
||||
|
||||
impl CallCalleeMatcher {
|
||||
pub fn new(pattern: impl Into<String>) -> Result<Self, regex::Error> {
|
||||
let pattern = pattern.into();
|
||||
let regex = Regex::new(pattern.as_ref())?;
|
||||
Ok(Self { pattern, regex })
|
||||
}
|
||||
|
||||
pub fn pattern(&self) -> &str {
|
||||
&self.pattern
|
||||
}
|
||||
|
||||
pub fn regex(&self) -> &Regex {
|
||||
&self.regex
|
||||
}
|
||||
}
|
||||
21
crates/ruff_linter/src/external/ast/runtime.rs
vendored
Normal file
21
crates/ruff_linter/src/external/ast/runtime.rs
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::external::ast::registry::ExternalLintRegistry;
|
||||
|
||||
/// Shareable handle to the external lint runtime state.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExternalLintRuntimeHandle {
|
||||
registry: Arc<ExternalLintRegistry>,
|
||||
}
|
||||
|
||||
impl ExternalLintRuntimeHandle {
|
||||
pub fn new(registry: ExternalLintRegistry) -> Self {
|
||||
Self {
|
||||
registry: Arc::new(registry),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registry(&self) -> &ExternalLintRegistry {
|
||||
&self.registry
|
||||
}
|
||||
}
|
||||
449
crates/ruff_linter/src/external/ast/target.rs
vendored
Normal file
449
crates/ruff_linter/src/external/ast/target.rs
vendored
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_python_ast::{Expr, Stmt};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
/// An AST node selector identifying which nodes a scripted rule should run against.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum AstTarget {
|
||||
Stmt(StmtKind),
|
||||
Expr(ExprKind),
|
||||
}
|
||||
|
||||
impl AstTarget {
|
||||
pub const fn kind(&self) -> AstNodeClass {
|
||||
match self {
|
||||
AstTarget::Stmt(..) => AstNodeClass::Stmt,
|
||||
AstTarget::Expr(..) => AstNodeClass::Expr,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn name(&self) -> &'static str {
|
||||
match self {
|
||||
AstTarget::Stmt(kind) => kind.as_str(),
|
||||
AstTarget::Expr(kind) => kind.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AstTarget {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AstTarget::Stmt(kind) => write!(f, "stmt:{}", kind.as_str()),
|
||||
AstTarget::Expr(kind) => write!(f, "expr:{}", kind.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AstTarget {
|
||||
type Err = AstTargetParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parse_target(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience wrapper that enables parsing `AstTarget` values directly from configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct AstTargetSpec(String);
|
||||
|
||||
impl AstTargetSpec {
|
||||
pub fn parse(&self) -> Result<AstTarget, AstTargetParseError> {
|
||||
self.0.as_str().parse()
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Broad AST node classes supported by scripted rules.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum AstNodeClass {
|
||||
Stmt,
|
||||
Expr,
|
||||
}
|
||||
|
||||
/// Statement kinds supported by scripted rules.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum StmtKind {
|
||||
FunctionDef,
|
||||
ClassDef,
|
||||
Return,
|
||||
Delete,
|
||||
TypeAlias,
|
||||
Assign,
|
||||
AugAssign,
|
||||
AnnAssign,
|
||||
For,
|
||||
While,
|
||||
If,
|
||||
With,
|
||||
Match,
|
||||
Raise,
|
||||
Try,
|
||||
Assert,
|
||||
Import,
|
||||
ImportFrom,
|
||||
Global,
|
||||
Nonlocal,
|
||||
Expr,
|
||||
Pass,
|
||||
Break,
|
||||
Continue,
|
||||
IpyEscapeCommand,
|
||||
}
|
||||
|
||||
impl StmtKind {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
StmtKind::FunctionDef => "FunctionDef",
|
||||
StmtKind::ClassDef => "ClassDef",
|
||||
StmtKind::Return => "Return",
|
||||
StmtKind::Delete => "Delete",
|
||||
StmtKind::TypeAlias => "TypeAlias",
|
||||
StmtKind::Assign => "Assign",
|
||||
StmtKind::AugAssign => "AugAssign",
|
||||
StmtKind::AnnAssign => "AnnAssign",
|
||||
StmtKind::For => "For",
|
||||
StmtKind::While => "While",
|
||||
StmtKind::If => "If",
|
||||
StmtKind::With => "With",
|
||||
StmtKind::Match => "Match",
|
||||
StmtKind::Raise => "Raise",
|
||||
StmtKind::Try => "Try",
|
||||
StmtKind::Assert => "Assert",
|
||||
StmtKind::Import => "Import",
|
||||
StmtKind::ImportFrom => "ImportFrom",
|
||||
StmtKind::Global => "Global",
|
||||
StmtKind::Nonlocal => "Nonlocal",
|
||||
StmtKind::Expr => "Expr",
|
||||
StmtKind::Pass => "Pass",
|
||||
StmtKind::Break => "Break",
|
||||
StmtKind::Continue => "Continue",
|
||||
StmtKind::IpyEscapeCommand => "IpyEscapeCommand",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(self, stmt: &Stmt) -> bool {
|
||||
matches!(
|
||||
(self, stmt),
|
||||
(StmtKind::FunctionDef, Stmt::FunctionDef(_))
|
||||
| (StmtKind::ClassDef, Stmt::ClassDef(_))
|
||||
| (StmtKind::Return, Stmt::Return(_))
|
||||
| (StmtKind::Delete, Stmt::Delete(_))
|
||||
| (StmtKind::TypeAlias, Stmt::TypeAlias(_))
|
||||
| (StmtKind::Assign, Stmt::Assign(_))
|
||||
| (StmtKind::AugAssign, Stmt::AugAssign(_))
|
||||
| (StmtKind::AnnAssign, Stmt::AnnAssign(_))
|
||||
| (StmtKind::For, Stmt::For(_))
|
||||
| (StmtKind::While, Stmt::While(_))
|
||||
| (StmtKind::If, Stmt::If(_))
|
||||
| (StmtKind::With, Stmt::With(_))
|
||||
| (StmtKind::Match, Stmt::Match(_))
|
||||
| (StmtKind::Raise, Stmt::Raise(_))
|
||||
| (StmtKind::Try, Stmt::Try(_))
|
||||
| (StmtKind::Assert, Stmt::Assert(_))
|
||||
| (StmtKind::Import, Stmt::Import(_))
|
||||
| (StmtKind::ImportFrom, Stmt::ImportFrom(_))
|
||||
| (StmtKind::Global, Stmt::Global(_))
|
||||
| (StmtKind::Nonlocal, Stmt::Nonlocal(_))
|
||||
| (StmtKind::Expr, Stmt::Expr(_))
|
||||
| (StmtKind::Pass, Stmt::Pass(_))
|
||||
| (StmtKind::Break, Stmt::Break(_))
|
||||
| (StmtKind::Continue, Stmt::Continue(_))
|
||||
| (StmtKind::IpyEscapeCommand, Stmt::IpyEscapeCommand(_))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StmtKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Stmt> for StmtKind {
|
||||
fn from(value: &Stmt) -> Self {
|
||||
match value {
|
||||
Stmt::FunctionDef(_) => StmtKind::FunctionDef,
|
||||
Stmt::ClassDef(_) => StmtKind::ClassDef,
|
||||
Stmt::Return(_) => StmtKind::Return,
|
||||
Stmt::Delete(_) => StmtKind::Delete,
|
||||
Stmt::TypeAlias(_) => StmtKind::TypeAlias,
|
||||
Stmt::Assign(_) => StmtKind::Assign,
|
||||
Stmt::AugAssign(_) => StmtKind::AugAssign,
|
||||
Stmt::AnnAssign(_) => StmtKind::AnnAssign,
|
||||
Stmt::For(_) => StmtKind::For,
|
||||
Stmt::While(_) => StmtKind::While,
|
||||
Stmt::If(_) => StmtKind::If,
|
||||
Stmt::With(_) => StmtKind::With,
|
||||
Stmt::Match(_) => StmtKind::Match,
|
||||
Stmt::Raise(_) => StmtKind::Raise,
|
||||
Stmt::Try(_) => StmtKind::Try,
|
||||
Stmt::Assert(_) => StmtKind::Assert,
|
||||
Stmt::Import(_) => StmtKind::Import,
|
||||
Stmt::ImportFrom(_) => StmtKind::ImportFrom,
|
||||
Stmt::Global(_) => StmtKind::Global,
|
||||
Stmt::Nonlocal(_) => StmtKind::Nonlocal,
|
||||
Stmt::Expr(_) => StmtKind::Expr,
|
||||
Stmt::Pass(_) => StmtKind::Pass,
|
||||
Stmt::Break(_) => StmtKind::Break,
|
||||
Stmt::Continue(_) => StmtKind::Continue,
|
||||
Stmt::IpyEscapeCommand(_) => StmtKind::IpyEscapeCommand,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expression kinds supported by scripted rules.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum ExprKind {
|
||||
Attribute,
|
||||
Await,
|
||||
BinOp,
|
||||
BoolOp,
|
||||
BooleanLiteral,
|
||||
BytesLiteral,
|
||||
Call,
|
||||
Compare,
|
||||
Dict,
|
||||
DictComp,
|
||||
EllipsisLiteral,
|
||||
FString,
|
||||
Generator,
|
||||
If,
|
||||
IpyEscapeCommand,
|
||||
Lambda,
|
||||
List,
|
||||
ListComp,
|
||||
Name,
|
||||
Named,
|
||||
NoneLiteral,
|
||||
NumberLiteral,
|
||||
Set,
|
||||
SetComp,
|
||||
Slice,
|
||||
Starred,
|
||||
StringLiteral,
|
||||
Subscript,
|
||||
Tuple,
|
||||
UnaryOp,
|
||||
Yield,
|
||||
YieldFrom,
|
||||
}
|
||||
|
||||
impl ExprKind {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ExprKind::Attribute => "Attribute",
|
||||
ExprKind::Await => "Await",
|
||||
ExprKind::BinOp => "BinOp",
|
||||
ExprKind::BoolOp => "BoolOp",
|
||||
ExprKind::BooleanLiteral => "BooleanLiteral",
|
||||
ExprKind::BytesLiteral => "BytesLiteral",
|
||||
ExprKind::Call => "Call",
|
||||
ExprKind::Compare => "Compare",
|
||||
ExprKind::Dict => "Dict",
|
||||
ExprKind::DictComp => "DictComp",
|
||||
ExprKind::EllipsisLiteral => "EllipsisLiteral",
|
||||
ExprKind::FString => "FString",
|
||||
ExprKind::Generator => "Generator",
|
||||
ExprKind::If => "If",
|
||||
ExprKind::IpyEscapeCommand => "IpyEscapeCommand",
|
||||
ExprKind::Lambda => "Lambda",
|
||||
ExprKind::List => "List",
|
||||
ExprKind::ListComp => "ListComp",
|
||||
ExprKind::Name => "Name",
|
||||
ExprKind::Named => "Named",
|
||||
ExprKind::NoneLiteral => "NoneLiteral",
|
||||
ExprKind::NumberLiteral => "NumberLiteral",
|
||||
ExprKind::Set => "Set",
|
||||
ExprKind::SetComp => "SetComp",
|
||||
ExprKind::Slice => "Slice",
|
||||
ExprKind::Starred => "Starred",
|
||||
ExprKind::StringLiteral => "StringLiteral",
|
||||
ExprKind::Subscript => "Subscript",
|
||||
ExprKind::Tuple => "Tuple",
|
||||
ExprKind::UnaryOp => "UnaryOp",
|
||||
ExprKind::Yield => "Yield",
|
||||
ExprKind::YieldFrom => "YieldFrom",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(self, expr: &Expr) -> bool {
|
||||
match self {
|
||||
ExprKind::Attribute => matches!(expr, Expr::Attribute(_)),
|
||||
ExprKind::Await => matches!(expr, Expr::Await(_)),
|
||||
ExprKind::BinOp => matches!(expr, Expr::BinOp(_)),
|
||||
ExprKind::BoolOp => matches!(expr, Expr::BoolOp(_)),
|
||||
ExprKind::BooleanLiteral => matches!(expr, Expr::BooleanLiteral(_)),
|
||||
ExprKind::BytesLiteral => matches!(expr, Expr::BytesLiteral(_)),
|
||||
ExprKind::Call => matches!(expr, Expr::Call(_)),
|
||||
ExprKind::Compare => matches!(expr, Expr::Compare(_)),
|
||||
ExprKind::Dict => matches!(expr, Expr::Dict(_)),
|
||||
ExprKind::DictComp => matches!(expr, Expr::DictComp(_)),
|
||||
ExprKind::EllipsisLiteral => matches!(expr, Expr::EllipsisLiteral(_)),
|
||||
ExprKind::FString => matches!(expr, Expr::FString(_) | Expr::TString(_)),
|
||||
ExprKind::Generator => matches!(expr, Expr::Generator(_)),
|
||||
ExprKind::If => matches!(expr, Expr::If(_)),
|
||||
ExprKind::IpyEscapeCommand => matches!(expr, Expr::IpyEscapeCommand(_)),
|
||||
ExprKind::Lambda => matches!(expr, Expr::Lambda(_)),
|
||||
ExprKind::List => matches!(expr, Expr::List(_)),
|
||||
ExprKind::ListComp => matches!(expr, Expr::ListComp(_)),
|
||||
ExprKind::Name => matches!(expr, Expr::Name(_)),
|
||||
ExprKind::Named => matches!(expr, Expr::Named(_)),
|
||||
ExprKind::NoneLiteral => matches!(expr, Expr::NoneLiteral(_)),
|
||||
ExprKind::NumberLiteral => matches!(expr, Expr::NumberLiteral(_)),
|
||||
ExprKind::Set => matches!(expr, Expr::Set(_)),
|
||||
ExprKind::SetComp => matches!(expr, Expr::SetComp(_)),
|
||||
ExprKind::Slice => matches!(expr, Expr::Slice(_)),
|
||||
ExprKind::Starred => matches!(expr, Expr::Starred(_)),
|
||||
ExprKind::StringLiteral => matches!(expr, Expr::StringLiteral(_)),
|
||||
ExprKind::Subscript => matches!(expr, Expr::Subscript(_)),
|
||||
ExprKind::Tuple => matches!(expr, Expr::Tuple(_)),
|
||||
ExprKind::UnaryOp => matches!(expr, Expr::UnaryOp(_)),
|
||||
ExprKind::Yield => matches!(expr, Expr::Yield(_)),
|
||||
ExprKind::YieldFrom => matches!(expr, Expr::YieldFrom(_)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ExprKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Expr> for ExprKind {
|
||||
fn from(value: &Expr) -> Self {
|
||||
match value {
|
||||
Expr::Attribute(_) => ExprKind::Attribute,
|
||||
Expr::Await(_) => ExprKind::Await,
|
||||
Expr::BinOp(_) => ExprKind::BinOp,
|
||||
Expr::BoolOp(_) => ExprKind::BoolOp,
|
||||
Expr::BooleanLiteral(_) => ExprKind::BooleanLiteral,
|
||||
Expr::BytesLiteral(_) => ExprKind::BytesLiteral,
|
||||
Expr::Call(_) => ExprKind::Call,
|
||||
Expr::Compare(_) => ExprKind::Compare,
|
||||
Expr::Dict(_) => ExprKind::Dict,
|
||||
Expr::DictComp(_) => ExprKind::DictComp,
|
||||
Expr::EllipsisLiteral(_) => ExprKind::EllipsisLiteral,
|
||||
Expr::FString(_) => ExprKind::FString,
|
||||
Expr::TString(_) => ExprKind::FString,
|
||||
Expr::Generator(_) => ExprKind::Generator,
|
||||
Expr::If(_) => ExprKind::If,
|
||||
Expr::IpyEscapeCommand(_) => ExprKind::IpyEscapeCommand,
|
||||
Expr::Lambda(_) => ExprKind::Lambda,
|
||||
Expr::List(_) => ExprKind::List,
|
||||
Expr::ListComp(_) => ExprKind::ListComp,
|
||||
Expr::Name(_) => ExprKind::Name,
|
||||
Expr::Named(_) => ExprKind::Named,
|
||||
Expr::NoneLiteral(_) => ExprKind::NoneLiteral,
|
||||
Expr::NumberLiteral(_) => ExprKind::NumberLiteral,
|
||||
Expr::Set(_) => ExprKind::Set,
|
||||
Expr::SetComp(_) => ExprKind::SetComp,
|
||||
Expr::Slice(_) => ExprKind::Slice,
|
||||
Expr::Starred(_) => ExprKind::Starred,
|
||||
Expr::StringLiteral(_) => ExprKind::StringLiteral,
|
||||
Expr::Subscript(_) => ExprKind::Subscript,
|
||||
Expr::Tuple(_) => ExprKind::Tuple,
|
||||
Expr::UnaryOp(_) => ExprKind::UnaryOp,
|
||||
Expr::Yield(_) => ExprKind::Yield,
|
||||
Expr::YieldFrom(_) => ExprKind::YieldFrom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AstTargetParseError {
|
||||
#[error("expected `stmt:<kind>` or `expr:<kind>` target selector")]
|
||||
MissingPrefix,
|
||||
#[error("unknown statement selector `{0}`")]
|
||||
UnknownStmtKind(String),
|
||||
#[error("unknown expression selector `{0}`")]
|
||||
UnknownExprKind(String),
|
||||
}
|
||||
|
||||
fn parse_target(raw: &str) -> Result<AstTarget, AstTargetParseError> {
|
||||
let (prefix, name) = raw
|
||||
.split_once(':')
|
||||
.ok_or(AstTargetParseError::MissingPrefix)?;
|
||||
match prefix {
|
||||
"stmt" => Ok(AstTarget::Stmt(parse_stmt_kind(name)?)),
|
||||
"expr" => Ok(AstTarget::Expr(parse_expr_kind(name)?)),
|
||||
_ => Err(AstTargetParseError::MissingPrefix),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_stmt_kind(name: &str) -> Result<StmtKind, AstTargetParseError> {
|
||||
match name {
|
||||
"FunctionDef" => Ok(StmtKind::FunctionDef),
|
||||
"ClassDef" => Ok(StmtKind::ClassDef),
|
||||
"Return" => Ok(StmtKind::Return),
|
||||
"Delete" => Ok(StmtKind::Delete),
|
||||
"TypeAlias" => Ok(StmtKind::TypeAlias),
|
||||
"Assign" => Ok(StmtKind::Assign),
|
||||
"AugAssign" => Ok(StmtKind::AugAssign),
|
||||
"AnnAssign" => Ok(StmtKind::AnnAssign),
|
||||
"For" => Ok(StmtKind::For),
|
||||
"While" => Ok(StmtKind::While),
|
||||
"If" => Ok(StmtKind::If),
|
||||
"With" => Ok(StmtKind::With),
|
||||
"Match" => Ok(StmtKind::Match),
|
||||
"Raise" => Ok(StmtKind::Raise),
|
||||
"Try" => Ok(StmtKind::Try),
|
||||
"Assert" => Ok(StmtKind::Assert),
|
||||
"Import" => Ok(StmtKind::Import),
|
||||
"ImportFrom" => Ok(StmtKind::ImportFrom),
|
||||
"Global" => Ok(StmtKind::Global),
|
||||
"Nonlocal" => Ok(StmtKind::Nonlocal),
|
||||
"Expr" => Ok(StmtKind::Expr),
|
||||
"Pass" => Ok(StmtKind::Pass),
|
||||
"Break" => Ok(StmtKind::Break),
|
||||
"Continue" => Ok(StmtKind::Continue),
|
||||
"IpyEscapeCommand" => Ok(StmtKind::IpyEscapeCommand),
|
||||
other => Err(AstTargetParseError::UnknownStmtKind(other.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_expr_kind(name: &str) -> Result<ExprKind, AstTargetParseError> {
|
||||
match name {
|
||||
"Attribute" => Ok(ExprKind::Attribute),
|
||||
"Await" => Ok(ExprKind::Await),
|
||||
"BinOp" => Ok(ExprKind::BinOp),
|
||||
"BoolOp" => Ok(ExprKind::BoolOp),
|
||||
"BooleanLiteral" => Ok(ExprKind::BooleanLiteral),
|
||||
"BytesLiteral" => Ok(ExprKind::BytesLiteral),
|
||||
"Call" => Ok(ExprKind::Call),
|
||||
"Compare" => Ok(ExprKind::Compare),
|
||||
"Dict" => Ok(ExprKind::Dict),
|
||||
"DictComp" => Ok(ExprKind::DictComp),
|
||||
"EllipsisLiteral" => Ok(ExprKind::EllipsisLiteral),
|
||||
"FString" => Ok(ExprKind::FString),
|
||||
"Generator" => Ok(ExprKind::Generator),
|
||||
"If" => Ok(ExprKind::If),
|
||||
"IpyEscapeCommand" => Ok(ExprKind::IpyEscapeCommand),
|
||||
"Lambda" => Ok(ExprKind::Lambda),
|
||||
"List" => Ok(ExprKind::List),
|
||||
"ListComp" => Ok(ExprKind::ListComp),
|
||||
"Name" => Ok(ExprKind::Name),
|
||||
"Named" => Ok(ExprKind::Named),
|
||||
"NoneLiteral" => Ok(ExprKind::NoneLiteral),
|
||||
"NumberLiteral" => Ok(ExprKind::NumberLiteral),
|
||||
"Set" => Ok(ExprKind::Set),
|
||||
"SetComp" => Ok(ExprKind::SetComp),
|
||||
"Slice" => Ok(ExprKind::Slice),
|
||||
"Starred" => Ok(ExprKind::Starred),
|
||||
"StringLiteral" => Ok(ExprKind::StringLiteral),
|
||||
"Subscript" => Ok(ExprKind::Subscript),
|
||||
"Tuple" => Ok(ExprKind::Tuple),
|
||||
"TString" => Ok(ExprKind::FString),
|
||||
"UnaryOp" => Ok(ExprKind::UnaryOp),
|
||||
"Yield" => Ok(ExprKind::Yield),
|
||||
"YieldFrom" => Ok(ExprKind::YieldFrom),
|
||||
other => Err(AstTargetParseError::UnknownExprKind(other.to_string())),
|
||||
}
|
||||
}
|
||||
78
crates/ruff_linter/src/external/error.rs
vendored
Normal file
78
crates/ruff_linter/src/external/error.rs
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::external::ast::target::AstTargetParseError;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExternalLinterError {
|
||||
#[error("failed to read external linter definition `{path}`: {source}")]
|
||||
Io {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to parse external linter definition `{path}`: {source}")]
|
||||
Parse {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: toml::de::Error,
|
||||
},
|
||||
|
||||
#[error("invalid rule code `{code}` for external linter `{linter}`")]
|
||||
InvalidRuleCode { linter: String, code: String },
|
||||
|
||||
#[error("unknown AST target `{target}` for external rule `{rule}` in linter `{linter}`")]
|
||||
// Targets must expand to one of the supported StmtKind or ExprKind enums; anything else is rejected.
|
||||
UnknownTarget {
|
||||
linter: String,
|
||||
rule: String,
|
||||
target: String,
|
||||
#[source]
|
||||
source: AstTargetParseError,
|
||||
},
|
||||
|
||||
#[error("duplicate rule code `{code}` in external linter `{linter}`")]
|
||||
DuplicateRule { linter: String, code: String },
|
||||
|
||||
#[error("duplicate external linter identifier `{id}`")]
|
||||
DuplicateLinter { id: String },
|
||||
|
||||
#[error("external linter `{id}` defines no rules")]
|
||||
EmptyLinter { id: String },
|
||||
|
||||
#[error("external rule `{rule}` in linter `{linter}` must declare at least one AST target")]
|
||||
MissingTargets { linter: String, rule: String },
|
||||
|
||||
#[error(
|
||||
"failed to read script `{path}` for external rule `{rule}` in linter `{linter}`: {source}"
|
||||
)]
|
||||
ScriptIo {
|
||||
linter: String,
|
||||
rule: String,
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("no script body provided for external rule `{rule}` in linter `{linter}`")]
|
||||
// Raised when we read a script file but it is empty or whitespace-only.
|
||||
MissingScriptBody { linter: String, rule: String },
|
||||
|
||||
#[error(
|
||||
"invalid `call-callee-regex` `{pattern}` for external rule `{rule}` in linter `{linter}`: {source}"
|
||||
)]
|
||||
InvalidCallCalleeRegex {
|
||||
linter: String,
|
||||
rule: String,
|
||||
pattern: String,
|
||||
#[source]
|
||||
source: regex::Error,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"external rule `{rule}` in linter `{linter}` declares `call-callee-regex` but does not target `expr:Call` nodes"
|
||||
)]
|
||||
CallCalleeRegexWithoutCallTarget { linter: String, rule: String },
|
||||
}
|
||||
26
crates/ruff_linter/src/external/mod.rs
vendored
Normal file
26
crates/ruff_linter/src/external/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
pub mod ast;
|
||||
pub mod error;
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PyprojectExternalLinterEntry {
|
||||
pub toml_path: PathBuf,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub use ast::definition::ExternalAstLinterFile;
|
||||
pub use ast::loader::{load_linter_from_entry, load_linter_into_registry};
|
||||
pub use ast::registry::{ExternalLintRegistry, RuleLocator};
|
||||
pub use ast::rule::{
|
||||
ExternalAstLinter, ExternalAstRule, ExternalAstRuleSpec, ExternalRuleCode, ExternalRuleScript,
|
||||
};
|
||||
pub use ast::runtime::ExternalLintRuntimeHandle;
|
||||
pub use ast::target::{AstNodeClass, AstTarget, AstTargetSpec, ExprKind, StmtKind};
|
||||
pub use error::ExternalLinterError;
|
||||
|
|
@ -26,6 +26,7 @@ mod cst;
|
|||
pub mod directives;
|
||||
mod doc_lines;
|
||||
mod docstrings;
|
||||
pub mod external;
|
||||
mod fix;
|
||||
pub mod fs;
|
||||
mod importer;
|
||||
|
|
|
|||
|
|
@ -7,10 +7,15 @@ use strum_macros::EnumIter;
|
|||
|
||||
use crate::codes::RuleIter;
|
||||
use crate::codes::{RuleCodePrefix, RuleGroup};
|
||||
use crate::external::ast::rule::ExternalRuleCode;
|
||||
use crate::registry::{Linter, Rule, RuleNamespace};
|
||||
use crate::rule_redirects::get_redirect;
|
||||
use crate::settings::types::PreviewMode;
|
||||
|
||||
fn looks_like_external_rule_code(s: &str) -> bool {
|
||||
ExternalRuleCode::matches_format(s)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RuleSelector {
|
||||
/// Select all rules (includes rules in preview if enabled)
|
||||
|
|
@ -33,6 +38,8 @@ pub enum RuleSelector {
|
|||
prefix: RuleCodePrefix,
|
||||
redirected_from: Option<&'static str>,
|
||||
},
|
||||
/// Select an external rule code.
|
||||
External { code: Box<str> },
|
||||
}
|
||||
|
||||
impl From<Linter> for RuleSelector {
|
||||
|
|
@ -70,8 +77,16 @@ impl FromStr for RuleSelector {
|
|||
None => (s, None),
|
||||
};
|
||||
|
||||
let (linter, code) =
|
||||
Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?;
|
||||
let Some((linter, code)) = Linter::parse_code(s) else {
|
||||
if looks_like_external_rule_code(s) {
|
||||
if ExternalRuleCode::new(s).is_ok() {
|
||||
return Ok(Self::External { code: s.into() });
|
||||
}
|
||||
|
||||
return Err(ParseError::External(s.to_string()));
|
||||
}
|
||||
return Err(ParseError::Unknown(s.to_string()));
|
||||
};
|
||||
|
||||
if code.is_empty() {
|
||||
return Ok(Self::Linter(linter));
|
||||
|
|
@ -119,10 +134,12 @@ 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.")]
|
||||
External(String),
|
||||
}
|
||||
|
||||
impl RuleSelector {
|
||||
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
|
||||
pub fn prefix_and_code(&self) -> (&str, &str) {
|
||||
match self {
|
||||
RuleSelector::All => ("", "ALL"),
|
||||
RuleSelector::C => ("", "C"),
|
||||
|
|
@ -131,6 +148,7 @@ impl RuleSelector {
|
|||
(prefix.linter().common_prefix(), prefix.short_code())
|
||||
}
|
||||
RuleSelector::Linter(l) => (l.common_prefix(), ""),
|
||||
RuleSelector::External { code } => ("", code.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -174,7 +192,14 @@ impl Visitor<'_> for SelectorVisitor {
|
|||
where
|
||||
E: de::Error,
|
||||
{
|
||||
FromStr::from_str(v).map_err(de::Error::custom)
|
||||
match FromStr::from_str(v) {
|
||||
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(err) => Err(de::Error::custom(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -198,6 +223,9 @@ impl RuleSelector {
|
|||
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
|
||||
RuleSelectorIter::Vec(prefix.clone().rules())
|
||||
}
|
||||
RuleSelector::External { .. } => {
|
||||
RuleSelectorIter::Vec(vec![Rule::ExternalLinter].into_iter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +252,7 @@ impl RuleSelector {
|
|||
|
||||
/// Returns true if this selector is exact i.e. selects a single rule by code
|
||||
pub fn is_exact(&self) -> bool {
|
||||
matches!(self, Self::Rule { .. })
|
||||
matches!(self, Self::Rule { .. } | Self::External { .. })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,6 +365,7 @@ impl RuleSelector {
|
|||
RuleSelector::C => Specificity::LinterGroup,
|
||||
RuleSelector::Linter(..) => Specificity::Linter,
|
||||
RuleSelector::Rule { .. } => Specificity::Rule,
|
||||
RuleSelector::External { .. } => Specificity::Rule,
|
||||
RuleSelector::Prefix { prefix, .. } => {
|
||||
let prefix: &'static str = prefix.short_code();
|
||||
match prefix.len() {
|
||||
|
|
@ -360,8 +389,16 @@ impl RuleSelector {
|
|||
"C" => Ok(Self::C),
|
||||
"T" => Ok(Self::T),
|
||||
_ => {
|
||||
let (linter, code) =
|
||||
Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?;
|
||||
let Some((linter, code)) = Linter::parse_code(s) else {
|
||||
if looks_like_external_rule_code(s) {
|
||||
if ExternalRuleCode::new(s).is_ok() {
|
||||
return Ok(Self::External { code: s.into() });
|
||||
}
|
||||
|
||||
return Err(ParseError::External(s.to_string()));
|
||||
}
|
||||
return Err(ParseError::Unknown(s.to_string()));
|
||||
};
|
||||
|
||||
if code.is_empty() {
|
||||
return Ok(Self::Linter(linter));
|
||||
|
|
@ -415,7 +452,7 @@ pub mod clap_completion {
|
|||
RuleSelector,
|
||||
codes::RuleCodePrefix,
|
||||
registry::{Linter, RuleNamespace},
|
||||
rule_selector::is_single_rule_selector,
|
||||
rule_selector::{ParseError, is_single_rule_selector},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -442,7 +479,12 @@ pub mod clap_completion {
|
|||
.to_str()
|
||||
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
|
||||
|
||||
value.parse().map_err(|_| {
|
||||
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."),
|
||||
),
|
||||
ParseError::Unknown(_) => {
|
||||
let mut error =
|
||||
clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
|
||||
if let Some(arg) = arg {
|
||||
|
|
@ -456,6 +498,7 @@ pub mod clap_completion {
|
|||
clap::error::ContextValue::String(value.to_string()),
|
||||
);
|
||||
error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
27
crates/ruff_linter/src/rules/ruff/rules/external_ast.rs
Normal file
27
crates/ruff_linter/src/rules/ruff/rules/external_ast.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use ruff_macros::{CacheKey, ViolationMetadata, derive_message_formats};
|
||||
|
||||
/// Diagnostics surfaced by external AST linters
|
||||
#[derive(Debug, Clone, PartialEq, Eq, CacheKey, ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.0")]
|
||||
pub(crate) struct ExternalLinter {
|
||||
pub rule_name: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ExternalLinter {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new(rule_name: impl Into<String>, message: String) -> Self {
|
||||
Self {
|
||||
rule_name: rule_name.into(),
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Violation for ExternalLinter {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let ExternalLinter { rule_name, message } = self;
|
||||
format!("{rule_name}: {message}")
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ pub(crate) use dataclass_enum::*;
|
|||
pub(crate) use decimal_from_float_literal::*;
|
||||
pub(crate) use default_factory_kwarg::*;
|
||||
pub(crate) use explicit_f_string_type_conversion::*;
|
||||
pub(crate) use external_ast::*;
|
||||
pub(crate) use falsy_dict_get_fallback::*;
|
||||
pub(crate) use function_call_in_dataclass_default::*;
|
||||
pub(crate) use if_key_in_dict_del::*;
|
||||
|
|
@ -73,6 +74,7 @@ mod dataclass_enum;
|
|||
mod decimal_from_float_literal;
|
||||
mod default_factory_kwarg;
|
||||
mod explicit_f_string_type_conversion;
|
||||
mod external_ast;
|
||||
mod falsy_dict_get_fallback;
|
||||
mod function_call_in_dataclass_default;
|
||||
mod if_key_in_dict_del;
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ impl ClientOptions {
|
|||
for rule in rules {
|
||||
match RuleSelector::from_str(rule) {
|
||||
Ok(selector) => known.push(selector),
|
||||
Err(ParseError::Unknown(_)) => unknown.push(rule),
|
||||
Err(ParseError::Unknown(_) | ParseError::External(_)) => unknown.push(rule),
|
||||
}
|
||||
}
|
||||
if !unknown.is_empty() {
|
||||
|
|
|
|||
3
ruff.schema.json
generated
3
ruff.schema.json
generated
|
|
@ -4052,6 +4052,9 @@
|
|||
"RUF2",
|
||||
"RUF20",
|
||||
"RUF200",
|
||||
"RUF3",
|
||||
"RUF30",
|
||||
"RUF300",
|
||||
"S",
|
||||
"S1",
|
||||
"S10",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue