Skip to content

Commit 57f73c9

Browse files
committed
[ruff][ext-lint] 3: set up PyO3 linter runtime
1 parent 4de331f commit 57f73c9

File tree

24 files changed

+2454
-332
lines changed

24 files changed

+2454
-332
lines changed

Cargo.lock

Lines changed: 285 additions & 284 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,10 @@ tikv-jemallocator = { workspace = true }
9191

9292
[lints]
9393
workspace = true
94+
95+
[features]
96+
default = []
97+
ext-lint = [
98+
"ruff_linter/ext-lint",
99+
"ruff_workspace/ext-lint",
100+
]

crates/ruff/tests/cli/ext_lint.rs

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
use anyhow::Result;
2+
3+
use crate::CliTest;
4+
5+
#[test]
6+
fn external_ast_reports_all_parse_errors() -> Result<()> {
7+
let test = CliTest::new()?;
8+
test.write_file(
9+
"lint/external/alpha.toml",
10+
r#"
11+
name = "Alpha"
12+
13+
[[rule]]
14+
code = "AXX001"
15+
name = "AlphaRule"
16+
targets = ["stmt:FunctionDef"]
17+
script = "rules/alpha.py"
18+
"#,
19+
)?;
20+
test.write_file(
21+
"lint/external/beta.toml",
22+
r#"
23+
name = "Beta"
24+
25+
[[rule]]
26+
code = "BXX001"
27+
name = "BetaRule"
28+
targets = ["stmt:FunctionDef"]
29+
script = "rules/beta.py"
30+
"#,
31+
)?;
32+
test.write_file("lint/external/rules/alpha.py", "def alpha(:\n")?;
33+
test.write_file("lint/external/rules/beta.py", "def beta(:\n")?;
34+
35+
let config = format!(
36+
r#"
37+
[lint.external-ast.alpha]
38+
path = "{}"
39+
40+
[lint.external-ast.beta]
41+
path = "{}"
42+
"#,
43+
test.root().join("lint/external/alpha.toml").display(),
44+
test.root().join("lint/external/beta.toml").display(),
45+
);
46+
test.write_file("ruff.toml", &config)?;
47+
48+
let output = test
49+
.command()
50+
.args(["check", "--config", "ruff.toml", "."])
51+
.output()?;
52+
53+
assert!(
54+
!output.status.success(),
55+
"command unexpectedly succeeded: {}",
56+
String::from_utf8_lossy(&output.stderr)
57+
);
58+
let stderr = std::str::from_utf8(&output.stderr)?;
59+
assert!(
60+
stderr.contains("alpha"),
61+
"stderr missing alpha error: {stderr}"
62+
);
63+
assert!(
64+
stderr.contains("beta"),
65+
"stderr missing beta error: {stderr}"
66+
);
67+
Ok(())
68+
}
69+
70+
#[test]
71+
fn external_ast_executes_rules() -> Result<()> {
72+
let test = CliTest::new()?;
73+
test.write_file(
74+
"lint/external/demo.toml",
75+
r#"
76+
name = "Demo"
77+
78+
[[rule]]
79+
code = "EXT001"
80+
name = "ExampleRule"
81+
targets = ["stmt:FunctionDef"]
82+
script = "rules/example.py"
83+
"#,
84+
)?;
85+
test.write_file(
86+
"lint/external/rules/example.py",
87+
r#"
88+
def check_stmt(node, ctx):
89+
if node["_kind"] == "FunctionDef":
90+
ctx.report("hello from script")
91+
"#,
92+
)?;
93+
let linter_path = test.root().join("lint/external/demo.toml");
94+
let config = format!(
95+
r#"
96+
[lint.external-ast.demo]
97+
path = "{}"
98+
"#,
99+
linter_path.display()
100+
);
101+
test.write_file("ruff.toml", &config)?;
102+
test.write_file(
103+
"src/example.py",
104+
r#"
105+
def demo(value=0):
106+
return value
107+
"#,
108+
)?;
109+
110+
let output = test
111+
.command()
112+
.args([
113+
"check",
114+
"--config",
115+
"ruff.toml",
116+
"--select-external",
117+
"EXT001",
118+
"src/example.py",
119+
])
120+
.output()?;
121+
assert!(
122+
!output.status.success(),
123+
"command unexpectedly succeeded: {}",
124+
String::from_utf8_lossy(&output.stderr)
125+
);
126+
let stdout = String::from_utf8_lossy(&output.stdout);
127+
assert!(stdout.contains("EXT001"), "stdout missing EXT001: {stdout}");
128+
assert!(
129+
stdout.contains("hello from script"),
130+
"stdout missing message: {stdout}"
131+
);
132+
Ok(())
133+
}
134+
135+
#[test]
136+
fn external_ast_respects_noqa() -> Result<()> {
137+
let test = CliTest::new()?;
138+
test.write_file(
139+
"lint/external/demo.toml",
140+
r#"
141+
name = "Demo"
142+
143+
[[rule]]
144+
code = "EXT001"
145+
name = "ExampleRule"
146+
targets = ["stmt:FunctionDef"]
147+
script = "rules/example.py"
148+
"#,
149+
)?;
150+
test.write_file(
151+
"lint/external/rules/example.py",
152+
r#"
153+
def check_stmt(node, ctx):
154+
if node["_kind"] == "FunctionDef":
155+
ctx.report("hello from script")
156+
"#,
157+
)?;
158+
let linter_path = test.root().join("lint/external/demo.toml");
159+
let config = format!(
160+
r#"
161+
[lint.external-ast.demo]
162+
path = "{}"
163+
"#,
164+
linter_path.display()
165+
);
166+
test.write_file("ruff.toml", &config)?;
167+
test.write_file(
168+
"src/example.py",
169+
r#"
170+
def demo(value=0): # noqa: EXT001
171+
return value
172+
"#,
173+
)?;
174+
175+
let output = test
176+
.command()
177+
.args([
178+
"check",
179+
"--config",
180+
"ruff.toml",
181+
"--select-external",
182+
"EXT001",
183+
"src/example.py",
184+
])
185+
.output()?;
186+
assert!(
187+
output.status.success(),
188+
"command unexpectedly failed: {}",
189+
String::from_utf8_lossy(&output.stderr)
190+
);
191+
let stdout = String::from_utf8_lossy(&output.stdout);
192+
assert!(
193+
!stdout.contains("EXT001"),
194+
"stdout unexpectedly reported EXT001: {stdout}"
195+
);
196+
Ok(())
197+
}
198+
199+
#[test]
200+
fn external_logging_linter_reports_interpolation() -> Result<()> {
201+
let test = CliTest::new()?;
202+
test.write_file(
203+
"lint/external/logging.toml",
204+
r#"
205+
name = "Logging"
206+
207+
[[rule]]
208+
code = "EXT801"
209+
name = "LoggingInterpolation"
210+
targets = ["expr:Call"]
211+
call_callee_regex = "(?i).*log.*\\.(debug|info|warning|warn|error|exception|critical|fatal)$"
212+
script = "logging/logging_interpolation.py"
213+
"#,
214+
)?;
215+
test.write_file(
216+
"lint/external/logging/logging_interpolation.py",
217+
include_str!("../fixtures/external/logging_linter.py"),
218+
)?;
219+
let linter_path = test.root().join("lint/external/logging.toml");
220+
let config = format!(
221+
r#"
222+
[lint.external-ast.logging]
223+
path = "{}"
224+
"#,
225+
linter_path.display()
226+
);
227+
test.write_file("ruff.toml", &config)?;
228+
test.write_file(
229+
"src/logging_cases.py",
230+
include_str!("../fixtures/external/logging_cases.py"),
231+
)?;
232+
233+
let output = test
234+
.command()
235+
.args([
236+
"check",
237+
"--config",
238+
"ruff.toml",
239+
"--select-external",
240+
"EXT801",
241+
"src/logging_cases.py",
242+
])
243+
.output()?;
244+
assert!(
245+
!output.status.success(),
246+
"command unexpectedly succeeded: {}",
247+
String::from_utf8_lossy(&output.stderr)
248+
);
249+
let stdout = String::from_utf8_lossy(&output.stdout);
250+
let hits: Vec<_> = stdout
251+
.lines()
252+
.filter(|line| line.contains("EXT801"))
253+
.collect();
254+
assert!(
255+
!hits.is_empty(),
256+
"stdout missing EXT801 logging violations: {stdout}"
257+
);
258+
assert_eq!(
259+
hits.len(),
260+
3,
261+
"expected three logging interpolation diagnostics: {stdout}"
262+
);
263+
assert!(
264+
hits.iter()
265+
.all(|line| line.contains("LoggingInterpolation: Logging message")),
266+
"stdout missing logging interpolation message: {stdout}"
267+
);
268+
Ok(())
269+
}

crates/ruff/tests/cli/lint.rs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,10 @@ script = "rules/example.py"
228228
test.write_file(
229229
"lint/external/rules/example.py",
230230
r#"
231-
def check():
232-
# placeholder script body
231+
def check_stmt(node, ctx):
232+
pass
233+
234+
def check_expr(node, ctx):
233235
pass
234236
"#,
235237
)?;
@@ -238,6 +240,9 @@ def check():
238240
r#"
239241
[lint.external-ast.demo]
240242
path = "{}"
243+
244+
[lint]
245+
select-external = ["EXT001"]
241246
"#,
242247
linter_path.display()
243248
);
@@ -289,8 +294,10 @@ script = "rules/example.py"
289294
test.write_file(
290295
"lint/external/rules/example.py",
291296
r#"
292-
def check():
293-
# placeholder script body
297+
def check_stmt(node, ctx):
298+
pass
299+
300+
def check_expr(node, ctx):
294301
pass
295302
"#,
296303
)?;
@@ -348,7 +355,7 @@ script = "rules/example.py"
348355
test.write_file(
349356
"lint/external/rules/example.py",
350357
r#"
351-
def check():
358+
def check_stmt(node, ctx):
352359
# placeholder script body
353360
pass
354361
"#,
@@ -665,11 +672,24 @@ path = "{}"
665672
"nested/example.py",
666673
])
667674
.output()?;
668-
assert!(
669-
output.status.success(),
670-
"command failed unexpectedly: {}",
671-
String::from_utf8_lossy(&output.stderr)
672-
);
675+
if cfg!(feature = "ext-lint") {
676+
assert!(
677+
!output.status.success(),
678+
"expected EXT001 diagnostics but command succeeded: {}",
679+
String::from_utf8_lossy(&output.stderr)
680+
);
681+
let stdout = String::from_utf8_lossy(&output.stdout);
682+
assert!(
683+
stdout.contains("EXT001"),
684+
"stdout missing EXT001 diagnostics: {stdout}"
685+
);
686+
} else {
687+
assert!(
688+
output.status.success(),
689+
"command failed unexpectedly without ext-lint: {}",
690+
String::from_utf8_lossy(&output.stderr)
691+
);
692+
}
673693

674694
Ok(())
675695
}

crates/ruff/tests/cli/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use std::{
1616
use tempfile::TempDir;
1717

1818
mod analyze_graph;
19+
#[cfg(feature = "ext-lint")]
20+
mod ext_lint;
1921
mod format;
2022
mod lint;
2123

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
6+
def interpolate(logger, extra):
7+
logger.info("static value: %s", extra) # OK
8+
logger.info(f"f-string value: {extra}") # violation
9+
logger.warning("percent style %s" % extra) # violation # noqa: UP031
10+
logging.error("format {}".format(extra)) # violation # noqa: UP032
11+
logging.debug("plain literal") # OK

0 commit comments

Comments
 (0)