Skip to content

Commit 06aed22

Browse files
committed
Merge branch 'main' into dcreager/real-constraint-sets
* main: (21 commits) [ty] Literal promotion refactor (#20646) [ty] Add tests for nested generic functions (#20631) [`cli`] Add conflict between `--add-noqa` and `--diff` options (#20642) [ty] Ensure first-party search paths always appear in a sensible order (#20629) [ty] Use `typing.Self` for the first parameter of instance methods (#20517) [ty] Remove unnecessary `parsed_module()` calls (#20630) Remove `TextEmitter` (#20595) [ty] Use fully qualified names to distinguish ambiguous protocols in diagnostics (#20627) [ty] Ecosystem analyzer: relax timeout thresholds (#20626) [ty] Apply type mappings to functions eagerly (#20596) [ty] Improve disambiguation of class names in diagnostics (#20603) Add the *The Basics* title back to CONTRIBUTING.md (#20624) [`playground`] Fix quick fixes for empty ranges in playground (#20599) Update dependency ruff to v0.13.2 (#20622) [`ruff`] Fix minor typos in doc comments (#20623) Update dependency PyYAML to v6.0.3 (#20621) Update cargo-bins/cargo-binstall action to v1.15.6 (#20620) Fixed documentation for try_consider_else (#20587) [ty] Use `Top` materializations for `TypeIs` special form (#20591) [ty] Simplify `Any | (Any & T)` to `Any` (#20593) ...
2 parents c73995a + b483d3b commit 06aed22

File tree

113 files changed

+2442
-1131
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+2442
-1131
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ jobs:
452452
- name: "Install Rust toolchain"
453453
run: rustup show
454454
- name: "Install cargo-binstall"
455-
uses: cargo-bins/cargo-binstall@20aa316bab4942180bbbabe93237858e8d77f1ed # v1.15.5
455+
uses: cargo-bins/cargo-binstall@38e8f5e4c386b611d51e8aa997b9a06a3c8eb67a # v1.15.6
456456
- name: "Install cargo-fuzz"
457457
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
458458
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
@@ -703,7 +703,7 @@ jobs:
703703
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
704704
with:
705705
persist-credentials: false
706-
- uses: cargo-bins/cargo-binstall@20aa316bab4942180bbbabe93237858e8d77f1ed # v1.15.5
706+
- uses: cargo-bins/cargo-binstall@38e8f5e4c386b611d51e8aa997b9a06a3c8eb67a # v1.15.6
707707
- run: cargo binstall --no-confirm cargo-shear
708708
- run: cargo shear
709709

.github/workflows/ty-ecosystem-analyzer.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
6565
cd ..
6666
67-
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@fc0f612798710b0dd69bb7528bc9b361dc60bd43"
67+
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@6ce3a609575bc84eaf5d247739529c60b6c2ae5b"
6868
6969
ecosystem-analyzer \
7070
--repository ruff \

.github/workflows/ty-ecosystem-report.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
5050
cd ..
5151
52-
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@fc0f612798710b0dd69bb7528bc9b361dc60bd43"
52+
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@6ce3a609575bc84eaf5d247739529c60b6c2ae5b"
5353
5454
ecosystem-analyzer \
5555
--verbose \

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ exploration of new features, we will often close these pull requests immediately
3737
new feature to ruff creates a long-term maintenance burden and requires strong consensus from the ruff
3838
team before it is appropriate to begin work on an implementation.
3939

40+
## The Basics
41+
4042
### Prerequisites
4143

4244
Ruff is written in Rust. You'll need to install the

crates/ruff/src/args.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ pub struct CheckCommand {
416416
conflicts_with = "stdin_filename",
417417
conflicts_with = "watch",
418418
conflicts_with = "fix",
419+
conflicts_with = "diff",
419420
)]
420421
pub add_noqa: bool,
421422
/// See the files Ruff will be run against with the current settings.

crates/ruff/src/commands/check.rs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@ mod test {
227227
use rustc_hash::FxHashMap;
228228
use tempfile::TempDir;
229229

230-
use ruff_linter::message::{Emitter, EmitterContext, TextEmitter};
230+
use ruff_db::diagnostic::{DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics};
231+
use ruff_linter::message::EmitterContext;
231232
use ruff_linter::registry::Rule;
232233
use ruff_linter::settings::types::UnsafeFixes;
233234
use ruff_linter::settings::{LinterSettings, flags};
@@ -280,19 +281,16 @@ mod test {
280281
UnsafeFixes::Enabled,
281282
)
282283
.unwrap();
283-
let mut output = Vec::new();
284-
285-
TextEmitter::default()
286-
.with_show_fix_status(true)
287-
.with_color(false)
288-
.emit(
289-
&mut output,
290-
&diagnostics.inner,
291-
&EmitterContext::new(&FxHashMap::default()),
292-
)
293-
.unwrap();
294-
295-
let messages = String::from_utf8(output).unwrap();
284+
285+
let config = DisplayDiagnosticConfig::default()
286+
.format(DiagnosticFormat::Concise)
287+
.hide_severity(true);
288+
let messages = DisplayDiagnostics::new(
289+
&EmitterContext::new(&FxHashMap::default()),
290+
&config,
291+
&diagnostics.inner,
292+
)
293+
.to_string();
296294

297295
insta::with_settings!({
298296
omit_expression => true,

crates/ruff/src/printer.rs

Lines changed: 37 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ use ruff_linter::linter::FixTable;
1010
use serde::Serialize;
1111

1212
use ruff_db::diagnostic::{
13-
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
14-
DisplayGithubDiagnostics, GithubRenderer, SecondaryCode,
13+
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
1514
};
1615
use ruff_linter::fs::relativize_path;
1716
use ruff_linter::logging::LogLevel;
18-
use ruff_linter::message::{Emitter, EmitterContext, GroupedEmitter, SarifEmitter, TextEmitter};
17+
use ruff_linter::message::{EmitterContext, render_diagnostics};
1918
use ruff_linter::notify_user;
2019
use ruff_linter::settings::flags::{self};
2120
use ruff_linter::settings::types::{OutputFormat, UnsafeFixes};
@@ -225,86 +224,28 @@ impl Printer {
225224
let context = EmitterContext::new(&diagnostics.notebook_indexes);
226225
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
227226

228-
let config = DisplayDiagnosticConfig::default().preview(preview);
229-
230-
match self.format {
231-
OutputFormat::Json => {
232-
let config = config.format(DiagnosticFormat::Json);
233-
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
234-
write!(writer, "{value}")?;
235-
}
236-
OutputFormat::Rdjson => {
237-
let config = config.format(DiagnosticFormat::Rdjson);
238-
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
239-
write!(writer, "{value}")?;
240-
}
241-
OutputFormat::JsonLines => {
242-
let config = config.format(DiagnosticFormat::JsonLines);
243-
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
244-
write!(writer, "{value}")?;
245-
}
246-
OutputFormat::Junit => {
247-
let config = config.format(DiagnosticFormat::Junit);
248-
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
249-
write!(writer, "{value}")?;
250-
}
251-
OutputFormat::Concise | OutputFormat::Full => {
252-
TextEmitter::default()
253-
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
254-
.with_show_fix_diff(self.format == OutputFormat::Full && preview)
255-
.with_show_source(self.format == OutputFormat::Full)
256-
.with_fix_applicability(self.unsafe_fixes.required_applicability())
257-
.with_preview(preview)
258-
.emit(writer, &diagnostics.inner, &context)?;
259-
260-
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
261-
if !diagnostics.fixed.is_empty() {
262-
writeln!(writer)?;
263-
print_fix_summary(writer, &diagnostics.fixed)?;
264-
writeln!(writer)?;
265-
}
227+
let config = DisplayDiagnosticConfig::default()
228+
.preview(preview)
229+
.hide_severity(true)
230+
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
231+
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
232+
.with_fix_applicability(self.unsafe_fixes.required_applicability())
233+
.show_fix_diff(preview);
234+
235+
render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?;
236+
237+
if matches!(
238+
self.format,
239+
OutputFormat::Full | OutputFormat::Concise | OutputFormat::Grouped
240+
) {
241+
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
242+
if !diagnostics.fixed.is_empty() {
243+
writeln!(writer)?;
244+
print_fix_summary(writer, &diagnostics.fixed)?;
245+
writeln!(writer)?;
266246
}
267-
268-
self.write_summary_text(writer, diagnostics)?;
269-
}
270-
OutputFormat::Grouped => {
271-
GroupedEmitter::default()
272-
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
273-
.with_unsafe_fixes(self.unsafe_fixes)
274-
.emit(writer, &diagnostics.inner, &context)?;
275-
276-
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
277-
if !diagnostics.fixed.is_empty() {
278-
writeln!(writer)?;
279-
print_fix_summary(writer, &diagnostics.fixed)?;
280-
writeln!(writer)?;
281-
}
282-
}
283-
self.write_summary_text(writer, diagnostics)?;
284-
}
285-
OutputFormat::Github => {
286-
let renderer = GithubRenderer::new(&context, "Ruff");
287-
let value = DisplayGithubDiagnostics::new(&renderer, &diagnostics.inner);
288-
write!(writer, "{value}")?;
289-
}
290-
OutputFormat::Gitlab => {
291-
let config = config.format(DiagnosticFormat::Gitlab);
292-
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
293-
write!(writer, "{value}")?;
294-
}
295-
OutputFormat::Pylint => {
296-
let config = config.format(DiagnosticFormat::Pylint);
297-
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
298-
write!(writer, "{value}")?;
299-
}
300-
OutputFormat::Azure => {
301-
let config = config.format(DiagnosticFormat::Azure);
302-
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
303-
write!(writer, "{value}")?;
304-
}
305-
OutputFormat::Sarif => {
306-
SarifEmitter.emit(writer, &diagnostics.inner, &context)?;
307247
}
248+
self.write_summary_text(writer, diagnostics)?;
308249
}
309250

310251
writer.flush()?;
@@ -448,11 +389,22 @@ impl Printer {
448389
}
449390

450391
let context = EmitterContext::new(&diagnostics.notebook_indexes);
451-
TextEmitter::default()
392+
let format = if preview {
393+
DiagnosticFormat::Full
394+
} else {
395+
DiagnosticFormat::Concise
396+
};
397+
let config = DisplayDiagnosticConfig::default()
398+
.hide_severity(true)
399+
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
452400
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
453-
.with_show_source(preview)
454-
.with_fix_applicability(self.unsafe_fixes.required_applicability())
455-
.emit(writer, &diagnostics.inner, &context)?;
401+
.format(format)
402+
.with_fix_applicability(self.unsafe_fixes.required_applicability());
403+
write!(
404+
writer,
405+
"{}",
406+
DisplayDiagnostics::new(&context, &config, &diagnostics.inner)
407+
)?;
456408
}
457409
writer.flush()?;
458410

crates/ruff/tests/lint.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6199,6 +6199,36 @@ match 42: # invalid-syntax
61996199
Ok(())
62006200
}
62016201

6202+
#[test_case::test_case("concise"; "concise_show_fixes")]
6203+
#[test_case::test_case("full"; "full_show_fixes")]
6204+
#[test_case::test_case("grouped"; "grouped_show_fixes")]
6205+
fn output_format_show_fixes(output_format: &str) -> Result<()> {
6206+
let tempdir = TempDir::new()?;
6207+
let input = tempdir.path().join("input.py");
6208+
fs::write(&input, "import os # F401")?;
6209+
6210+
let snapshot = format!("output_format_show_fixes_{output_format}");
6211+
6212+
assert_cmd_snapshot!(
6213+
snapshot,
6214+
Command::new(get_cargo_bin(BIN_NAME))
6215+
.args([
6216+
"check",
6217+
"--no-cache",
6218+
"--output-format",
6219+
output_format,
6220+
"--select",
6221+
"F401",
6222+
"--fix",
6223+
"--show-fixes",
6224+
"input.py",
6225+
])
6226+
.current_dir(&tempdir),
6227+
);
6228+
6229+
Ok(())
6230+
}
6231+
62026232
#[test]
62036233
fn up045_nested_optional_flatten_all() {
62046234
let contents = "\

crates/ruff/tests/snapshots/integration_test__rule_f401.snap

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,43 @@ import some_module
4444
__all__ = ["some_module"]
4545
```
4646

47+
## Preview
48+
When [preview] is enabled (and certain simplifying assumptions
49+
are met), we analyze all import statements for a given module
50+
when determining whether an import is used, rather than simply
51+
the last of these statements. This can result in both different and
52+
more import statements being marked as unused.
53+
54+
For example, if a module consists of
55+
56+
```python
57+
import a
58+
import a.b
59+
```
60+
61+
then both statements are marked as unused under [preview], whereas
62+
only the second is marked as unused under stable behavior.
63+
64+
As another example, if a module consists of
65+
66+
```python
67+
import a.b
68+
import a
69+
70+
a.b.foo()
71+
```
72+
73+
then a diagnostic will only be emitted for the first line under [preview],
74+
whereas a diagnostic would only be emitted for the second line under
75+
stable behavior.
76+
77+
Note that this behavior is somewhat subjective and is designed
78+
to conform to the developer's intuition rather than Python's actual
79+
execution. To wit, the statement `import a.b` automatically executes
80+
`import a`, so in some sense `import a` is _always_ redundant
81+
in the presence of `import a.b`.
82+
83+
4784
## Fix safety
4885

4986
Fixes to remove unused imports are safe, except in `__init__.py` files.
@@ -96,4 +133,6 @@ else:
96133
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
97134
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
98135

136+
[preview]: https://docs.astral.sh/ruff/preview/
137+
99138
----- stderr -----

crates/ruff/tests/snapshots/lint__output_format_sarif.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ exit_code: 1
119119
"rules": [
120120
{
121121
"fullDescription": {
122-
"text": "## 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## 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"
122+
"text": "## 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"
123123
},
124124
"help": {
125125
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"

0 commit comments

Comments
 (0)