Skip to content

Commit c2ae9c7

Browse files
authored
feat: 'ruff rule' provides more easily parsable JSON ouput (#20168)
1 parent 5a1201b commit c2ae9c7

File tree

6 files changed

+218
-2
lines changed

6 files changed

+218
-2
lines changed

crates/ruff/src/commands/rule.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use serde::{Serialize, Serializer};
77
use strum::IntoEnumIterator;
88

99
use ruff_linter::FixAvailability;
10+
use ruff_linter::codes::RuleGroup;
1011
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
1112

1213
use crate::args::HelpFormat;
@@ -19,9 +20,11 @@ struct Explanation<'a> {
1920
summary: &'a str,
2021
message_formats: &'a [&'a str],
2122
fix: String,
23+
fix_availability: FixAvailability,
2224
#[expect(clippy::struct_field_names)]
2325
explanation: Option<&'a str>,
2426
preview: bool,
27+
status: RuleGroup,
2528
}
2629

2730
impl<'a> Explanation<'a> {
@@ -36,8 +39,10 @@ impl<'a> Explanation<'a> {
3639
summary: rule.message_formats()[0],
3740
message_formats: rule.message_formats(),
3841
fix,
42+
fix_availability: rule.fixable(),
3943
explanation: rule.explanation(),
4044
preview: rule.is_preview(),
45+
status: rule.group(),
4146
}
4247
}
4348
}

crates/ruff/tests/integration_test.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,16 @@ fn rule_f401() {
951951
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401"]));
952952
}
953953

954+
#[test]
955+
fn rule_f401_output_json() {
956+
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401", "--output-format", "json"]));
957+
}
958+
959+
#[test]
960+
fn rule_f401_output_text() {
961+
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401", "--output-format", "text"]));
962+
}
963+
954964
#[test]
955965
fn rule_invalid_rule_name() {
956966
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404"]), @r"
@@ -965,6 +975,34 @@ fn rule_invalid_rule_name() {
965975
");
966976
}
967977

978+
#[test]
979+
fn rule_invalid_rule_name_output_json() {
980+
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404", "--output-format", "json"]), @r"
981+
success: false
982+
exit_code: 2
983+
----- stdout -----
984+
985+
----- stderr -----
986+
error: invalid value 'RUF404' for '[RULE]'
987+
988+
For more information, try '--help'.
989+
");
990+
}
991+
992+
#[test]
993+
fn rule_invalid_rule_name_output_text() {
994+
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404", "--output-format", "text"]), @r"
995+
success: false
996+
exit_code: 2
997+
----- stdout -----
998+
999+
----- stderr -----
1000+
error: invalid value 'RUF404' for '[RULE]'
1001+
1002+
For more information, try '--help'.
1003+
");
1004+
}
1005+
9681006
#[test]
9691007
fn show_statistics() {
9701008
let mut cmd = RuffCheck::default()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
source: crates/ruff/tests/integration_test.rs
3+
info:
4+
program: ruff
5+
args:
6+
- rule
7+
- F401
8+
- "--output-format"
9+
- json
10+
---
11+
success: true
12+
exit_code: 0
13+
----- stdout -----
14+
{
15+
"name": "unused-import",
16+
"code": "F401",
17+
"linter": "Pyflakes",
18+
"summary": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability",
19+
"message_formats": [
20+
"`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability",
21+
"`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias",
22+
"`{name}` imported but unused"
23+
],
24+
"fix": "Fix is sometimes available.",
25+
"fix_availability": "Sometimes",
26+
"explanation": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Preview\nWhen [preview] is enabled (and certain simplifying assumptions\nare met), we analyze all import statements for a given module\nwhen determining whether an import is used, rather than simply\nthe last of these statements. This can result in both different and\nmore import statements being marked as unused.\n\nFor example, if a module consists of\n\n```python\nimport a\nimport a.b\n```\n\nthen both statements are marked as unused under [preview], whereas\nonly the second is marked as unused under stable behavior.\n\nAs another example, if a module consists of\n\n```python\nimport a.b\nimport a\n\na.b.foo()\n```\n\nthen a diagnostic will only be emitted for the first line under [preview],\nwhereas a diagnostic would only be emitted for the second line under\nstable behavior.\n\nNote that this behavior is somewhat subjective and is designed\nto conform to the developer's intuition rather than Python's actual\nexecution. To wit, the statement `import a.b` automatically executes\n`import a`, so in some sense `import a` is _always_ redundant\nin the presence of `import a.b`.\n\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n\n[preview]: https://docs.astral.sh/ruff/preview/\n",
27+
"preview": false,
28+
"status": "Stable"
29+
}
30+
----- stderr -----
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
---
2+
source: crates/ruff/tests/integration_test.rs
3+
info:
4+
program: ruff
5+
args:
6+
- rule
7+
- F401
8+
- "--output-format"
9+
- text
10+
---
11+
success: true
12+
exit_code: 0
13+
----- stdout -----
14+
# unused-import (F401)
15+
16+
Derived from the **Pyflakes** linter.
17+
18+
Fix is sometimes available.
19+
20+
## What it does
21+
Checks for unused imports.
22+
23+
## Why is this bad?
24+
Unused imports add a performance overhead at runtime, and risk creating
25+
import cycles. They also increase the cognitive load of reading the code.
26+
27+
If an import statement is used to check for the availability or existence
28+
of a module, consider using `importlib.util.find_spec` instead.
29+
30+
If an import statement is used to re-export a symbol as part of a module's
31+
public interface, consider using a "redundant" import alias, which
32+
instructs Ruff (and other tools) to respect the re-export, and avoid
33+
marking it as unused, as in:
34+
35+
```python
36+
from module import member as member
37+
```
38+
39+
Alternatively, you can use `__all__` to declare a symbol as part of the module's
40+
interface, as in:
41+
42+
```python
43+
# __init__.py
44+
import some_module
45+
46+
__all__ = ["some_module"]
47+
```
48+
49+
## Preview
50+
When [preview] is enabled (and certain simplifying assumptions
51+
are met), we analyze all import statements for a given module
52+
when determining whether an import is used, rather than simply
53+
the last of these statements. This can result in both different and
54+
more import statements being marked as unused.
55+
56+
For example, if a module consists of
57+
58+
```python
59+
import a
60+
import a.b
61+
```
62+
63+
then both statements are marked as unused under [preview], whereas
64+
only the second is marked as unused under stable behavior.
65+
66+
As another example, if a module consists of
67+
68+
```python
69+
import a.b
70+
import a
71+
72+
a.b.foo()
73+
```
74+
75+
then a diagnostic will only be emitted for the first line under [preview],
76+
whereas a diagnostic would only be emitted for the second line under
77+
stable behavior.
78+
79+
Note that this behavior is somewhat subjective and is designed
80+
to conform to the developer's intuition rather than Python's actual
81+
execution. To wit, the statement `import a.b` automatically executes
82+
`import a`, so in some sense `import a` is _always_ redundant
83+
in the presence of `import a.b`.
84+
85+
86+
## Fix safety
87+
88+
Fixes to remove unused imports are safe, except in `__init__.py` files.
89+
90+
Applying fixes to `__init__.py` files is currently in preview. The fix offered depends on the
91+
type of the unused import. Ruff will suggest a safe fix to export first-party imports with
92+
either a redundant alias or, if already present in the file, an `__all__` entry. If multiple
93+
`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix
94+
to remove third-party and standard library imports -- the fix is unsafe because the module's
95+
interface changes.
96+
97+
See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)
98+
for more details on how Ruff
99+
determines whether an import is first or third-party.
100+
101+
## Example
102+
103+
```python
104+
import numpy as np # unused import
105+
106+
107+
def area(radius):
108+
return 3.14 * radius**2
109+
```
110+
111+
Use instead:
112+
113+
```python
114+
def area(radius):
115+
return 3.14 * radius**2
116+
```
117+
118+
To check the availability of a module, use `importlib.util.find_spec`:
119+
120+
```python
121+
from importlib.util import find_spec
122+
123+
if find_spec("numpy") is not None:
124+
print("numpy is installed")
125+
else:
126+
print("numpy is not installed")
127+
```
128+
129+
## Options
130+
- `lint.ignore-init-module-imports`
131+
- `lint.pyflakes.allowed-unused-imports`
132+
133+
## References
134+
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
135+
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
136+
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
137+
138+
[preview]: https://docs.astral.sh/ruff/preview/
139+
140+
----- stderr -----

crates/ruff_linter/src/codes.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use std::fmt::Formatter;
66

77
use ruff_db::diagnostic::SecondaryCode;
8+
use serde::Serialize;
89
use strum_macros::EnumIter;
910

1011
use crate::registry::Linter;
@@ -74,7 +75,7 @@ impl serde::Serialize for NoqaCode {
7475
}
7576
}
7677

77-
#[derive(Debug, Copy, Clone)]
78+
#[derive(Debug, Copy, Clone, Serialize)]
7879
pub enum RuleGroup {
7980
/// The rule is stable.
8081
Stable,

crates/ruff_linter/src/violation.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use std::fmt::{Debug, Display};
22

3+
use serde::Serialize;
4+
35
use ruff_db::diagnostic::Diagnostic;
46
use ruff_source_file::SourceFile;
57
use ruff_text_size::TextRange;
68

79
use crate::{codes::Rule, message::create_lint_diagnostic};
810

9-
#[derive(Debug, Copy, Clone)]
11+
#[derive(Debug, Copy, Clone, Serialize)]
1012
pub enum FixAvailability {
1113
Sometimes,
1214
Always,

0 commit comments

Comments
 (0)