Skip to content

Commit 6e2f987

Browse files
committed
Merge branch 'main' into dcreager/unpack-tuple
* main: [ty] Make tuple instantiations sound (#18987) [`flake8-pyi`] Expand `Optional[A]` to `A | None` (`PYI016`) (#18572) Convert `OldDiagnostic::noqa_code` to an `Option<String>` (#18946) [ty] Fix playground (#18986)
2 parents be46c62 + a50a993 commit 6e2f987

File tree

42 files changed

+3067
-190
lines changed

Some content is hidden

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

42 files changed

+3067
-190
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff/src/printer.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use anyhow::Result;
66
use bitflags::bitflags;
77
use colored::Colorize;
88
use itertools::{Itertools, iterate};
9-
use ruff_linter::codes::NoqaCode;
109
use ruff_linter::linter::FixTable;
1110
use serde::Serialize;
1211

@@ -15,7 +14,7 @@ use ruff_linter::logging::LogLevel;
1514
use ruff_linter::message::{
1615
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
1716
JsonEmitter, JsonLinesEmitter, JunitEmitter, OldDiagnostic, PylintEmitter, RdjsonEmitter,
18-
SarifEmitter, TextEmitter,
17+
SarifEmitter, SecondaryCode, TextEmitter,
1918
};
2019
use ruff_linter::notify_user;
2120
use ruff_linter::settings::flags::{self};
@@ -36,8 +35,8 @@ bitflags! {
3635
}
3736

3837
#[derive(Serialize)]
39-
struct ExpandedStatistics {
40-
code: Option<NoqaCode>,
38+
struct ExpandedStatistics<'a> {
39+
code: Option<&'a SecondaryCode>,
4140
name: &'static str,
4241
count: usize,
4342
fixable: bool,
@@ -303,11 +302,12 @@ impl Printer {
303302
let statistics: Vec<ExpandedStatistics> = diagnostics
304303
.inner
305304
.iter()
306-
.map(|message| (message.noqa_code(), message))
305+
.map(|message| (message.secondary_code(), message))
307306
.sorted_by_key(|(code, message)| (*code, message.fixable()))
308307
.fold(
309308
vec![],
310-
|mut acc: Vec<((Option<NoqaCode>, &OldDiagnostic), usize)>, (code, message)| {
309+
|mut acc: Vec<((Option<&SecondaryCode>, &OldDiagnostic), usize)>,
310+
(code, message)| {
311311
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
312312
if *prev_code == code {
313313
*count += 1;
@@ -349,12 +349,7 @@ impl Printer {
349349
);
350350
let code_width = statistics
351351
.iter()
352-
.map(|statistic| {
353-
statistic
354-
.code
355-
.map_or_else(String::new, |rule| rule.to_string())
356-
.len()
357-
})
352+
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
358353
.max()
359354
.unwrap();
360355
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
@@ -370,7 +365,8 @@ impl Printer {
370365
statistic.count.to_string().bold(),
371366
statistic
372367
.code
373-
.map_or_else(String::new, |rule| rule.to_string())
368+
.map(SecondaryCode::as_str)
369+
.unwrap_or_default()
374370
.red()
375371
.bold(),
376372
if any_fixable {

crates/ruff_linter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ colored = { workspace = true }
3838
fern = { workspace = true }
3939
glob = { workspace = true }
4040
globset = { workspace = true }
41+
hashbrown = { workspace = true }
4142
imperative = { workspace = true }
4243
is-macro = { workspace = true }
4344
is-wsl = { workspace = true }

crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,26 @@ def func2() -> str | str: # PYI016: Duplicate union member `str`
119119
# Technically, this falls into the domain of the rule but it is an unlikely edge case,
120120
# only works if you have from `__future__ import annotations` at the top of the file,
121121
# and stringified annotations are discouraged in stub files.
122-
field36: "int | str" | int # Ok
122+
field36: "int | str" | int # Ok
123+
124+
# https://github.com/astral-sh/ruff/issues/18546
125+
# Expand Optional[T] to Union[T, None]
126+
# OK
127+
field37: typing.Optional[int]
128+
field38: typing.Union[int, None]
129+
# equivalent to None
130+
field39: typing.Optional[None]
131+
# equivalent to int | None
132+
field40: typing.Union[typing.Optional[int], None]
133+
field41: typing.Optional[typing.Union[int, None]]
134+
field42: typing.Union[typing.Optional[int], typing.Optional[int]]
135+
field43: typing.Optional[int] | None
136+
field44: typing.Optional[int | None]
137+
field45: typing.Optional[int] | typing.Optional[int]
138+
# equivalent to int | dict | None
139+
field46: typing.Union[typing.Optional[int], typing.Optional[dict]]
140+
field47: typing.Optional[int] | typing.Optional[dict]
141+
142+
# avoid reporting twice
143+
field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
144+
field49: typing.Optional[complex | complex] | complex

crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,25 @@ field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error
111111

112112
# Test case for mixed union type
113113
field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error
114+
115+
# https://github.com/astral-sh/ruff/issues/18546
116+
# Expand Optional[T] to Union[T, None]
117+
# OK
118+
field37: typing.Optional[int]
119+
field38: typing.Union[int, None]
120+
# equivalent to None
121+
field39: typing.Optional[None]
122+
# equivalent to int | None
123+
field40: typing.Union[typing.Optional[int], None]
124+
field41: typing.Optional[typing.Union[int, None]]
125+
field42: typing.Union[typing.Optional[int], typing.Optional[int]]
126+
field43: typing.Optional[int] | None
127+
field44: typing.Optional[int | None]
128+
field45: typing.Optional[int] | typing.Optional[int]
129+
# equivalent to int | dict | None
130+
field46: typing.Union[typing.Optional[int], typing.Optional[dict]]
131+
field47: typing.Optional[int] | typing.Optional[dict]
132+
133+
# avoid reporting twice
134+
field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
135+
field49: typing.Optional[complex | complex] | complex

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use ruff_python_semantic::analyze::typing;
77
use ruff_text_size::Ranged;
88

99
use crate::checkers::ast::Checker;
10+
use crate::preview::is_optional_as_none_in_union_enabled;
1011
use crate::registry::Rule;
1112
use crate::rules::{
1213
airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
@@ -90,7 +91,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
9091
if checker.is_rule_enabled(Rule::UnnecessaryLiteralUnion) {
9192
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
9293
}
93-
if checker.is_rule_enabled(Rule::DuplicateUnionMember) {
94+
if checker.is_rule_enabled(Rule::DuplicateUnionMember)
95+
// Avoid duplicate checks inside `Optional`
96+
&& !(
97+
is_optional_as_none_in_union_enabled(checker.settings())
98+
&& checker.semantic.inside_optional()
99+
)
100+
{
94101
flake8_pyi::rules::duplicate_union_member(checker, expr);
95102
}
96103
if checker.is_rule_enabled(Rule::RedundantLiteralUnion) {
@@ -1430,6 +1437,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
14301437
if !checker.semantic.in_nested_union() {
14311438
if checker.is_rule_enabled(Rule::DuplicateUnionMember)
14321439
&& checker.semantic.in_type_definition()
1440+
// Avoid duplicate checks inside `Optional`
1441+
&& !(
1442+
is_optional_as_none_in_union_enabled(checker.settings())
1443+
&& checker.semantic.inside_optional()
1444+
)
14331445
{
14341446
flake8_pyi::rules::duplicate_union_member(checker, expr);
14351447
}

crates/ruff_linter/src/checkers/noqa.rs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,39 +35,34 @@ pub(crate) fn check_noqa(
3535
// Identify any codes that are globally exempted (within the current file).
3636
let file_noqa_directives =
3737
FileNoqaDirectives::extract(locator, comment_ranges, &settings.external, path);
38-
let exemption = FileExemption::from(&file_noqa_directives);
3938

4039
// Extract all `noqa` directives.
4140
let mut noqa_directives =
4241
NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator);
4342

43+
if file_noqa_directives.is_empty() && noqa_directives.is_empty() {
44+
return Vec::new();
45+
}
46+
47+
let exemption = FileExemption::from(&file_noqa_directives);
48+
4449
// Indices of diagnostics that were ignored by a `noqa` directive.
4550
let mut ignored_diagnostics = vec![];
4651

4752
// Remove any ignored diagnostics.
4853
'outer: for (index, diagnostic) in context.iter().enumerate() {
4954
// Can't ignore syntax errors.
50-
let Some(code) = diagnostic.noqa_code() else {
55+
let Some(code) = diagnostic.secondary_code() else {
5156
continue;
5257
};
5358

54-
if code == Rule::BlanketNOQA.noqa_code() {
59+
if *code == Rule::BlanketNOQA.noqa_code() {
5560
continue;
5661
}
5762

58-
match &exemption {
59-
FileExemption::All(_) => {
60-
// If the file is exempted, ignore all diagnostics.
61-
ignored_diagnostics.push(index);
62-
continue;
63-
}
64-
FileExemption::Codes(codes) => {
65-
// If the diagnostic is ignored by a global exemption, ignore it.
66-
if codes.contains(&&code) {
67-
ignored_diagnostics.push(index);
68-
continue;
69-
}
70-
}
63+
if exemption.contains_secondary_code(code) {
64+
ignored_diagnostics.push(index);
65+
continue;
7166
}
7267

7368
let noqa_offsets = diagnostic
@@ -82,13 +77,21 @@ pub(crate) fn check_noqa(
8277
{
8378
let suppressed = match &directive_line.directive {
8479
Directive::All(_) => {
85-
directive_line.matches.push(code);
80+
let Ok(rule) = Rule::from_code(code) else {
81+
debug_assert!(false, "Invalid secondary code `{code}`");
82+
continue;
83+
};
84+
directive_line.matches.push(rule);
8685
ignored_diagnostics.push(index);
8786
true
8887
}
8988
Directive::Codes(directive) => {
9089
if directive.includes(code) {
91-
directive_line.matches.push(code);
90+
let Ok(rule) = Rule::from_code(code) else {
91+
debug_assert!(false, "Invalid secondary code `{code}`");
92+
continue;
93+
};
94+
directive_line.matches.push(rule);
9295
ignored_diagnostics.push(index);
9396
true
9497
} else {
@@ -147,11 +150,11 @@ pub(crate) fn check_noqa(
147150

148151
if seen_codes.insert(original_code) {
149152
let is_code_used = if is_file_level {
150-
context
151-
.iter()
152-
.any(|diag| diag.noqa_code().is_some_and(|noqa| noqa == code))
153+
context.iter().any(|diag| {
154+
diag.secondary_code().is_some_and(|noqa| *noqa == code)
155+
})
153156
} else {
154-
matches.iter().any(|match_| *match_ == code)
157+
matches.iter().any(|match_| match_.noqa_code() == code)
155158
} || settings
156159
.external
157160
.iter()

crates/ruff_linter/src/codes.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ impl PartialEq<&str> for NoqaCode {
4646
}
4747
}
4848

49+
impl PartialEq<NoqaCode> for &str {
50+
fn eq(&self, other: &NoqaCode) -> bool {
51+
other.eq(self)
52+
}
53+
}
54+
4955
impl serde::Serialize for NoqaCode {
5056
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
5157
where

crates/ruff_linter/src/fix/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ fn apply_fixes<'a>(
6363
let mut source_map = SourceMap::default();
6464

6565
for (code, name, fix) in diagnostics
66-
.filter_map(|msg| msg.noqa_code().map(|code| (code, msg.name(), msg)))
66+
.filter_map(|msg| msg.secondary_code().map(|code| (code, msg.name(), msg)))
6767
.filter_map(|(code, name, diagnostic)| diagnostic.fix().map(|fix| (code, name, fix)))
6868
.sorted_by(|(_, name1, fix1), (_, name2, fix2)| cmp_fix(name1, name2, fix1, fix2))
6969
{

crates/ruff_linter/src/linter.rs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
use std::borrow::Cow;
2-
use std::collections::hash_map::Entry;
32
use std::path::Path;
43

54
use anyhow::{Result, anyhow};
65
use colored::Colorize;
76
use itertools::Itertools;
87
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
9-
use rustc_hash::FxHashMap;
8+
use rustc_hash::FxBuildHasher;
109

1110
use ruff_notebook::Notebook;
1211
use ruff_python_ast::{ModModule, PySourceType, PythonVersion};
@@ -23,10 +22,10 @@ use crate::checkers::imports::check_imports;
2322
use crate::checkers::noqa::check_noqa;
2423
use crate::checkers::physical_lines::check_physical_lines;
2524
use crate::checkers::tokens::check_tokens;
26-
use crate::codes::NoqaCode;
2725
use crate::directives::Directives;
2826
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
2927
use crate::fix::{FixResult, fix_file};
28+
use crate::message::SecondaryCode;
3029
use crate::noqa::add_noqa;
3130
use crate::package::PackageRoot;
3231
use crate::preview::is_py314_support_enabled;
@@ -95,33 +94,35 @@ struct FixCount {
9594

9695
/// A mapping from a noqa code to the corresponding lint name and a count of applied fixes.
9796
#[derive(Debug, Default, PartialEq)]
98-
pub struct FixTable(FxHashMap<NoqaCode, FixCount>);
97+
pub struct FixTable(hashbrown::HashMap<SecondaryCode, FixCount, rustc_hash::FxBuildHasher>);
9998

10099
impl FixTable {
101100
pub fn counts(&self) -> impl Iterator<Item = usize> {
102101
self.0.values().map(|fc| fc.count)
103102
}
104103

105-
pub fn entry(&mut self, code: NoqaCode) -> FixTableEntry {
106-
FixTableEntry(self.0.entry(code))
104+
pub fn entry<'a>(&'a mut self, code: &'a SecondaryCode) -> FixTableEntry<'a> {
105+
FixTableEntry(self.0.entry_ref(code))
107106
}
108107

109-
pub fn iter(&self) -> impl Iterator<Item = (NoqaCode, &'static str, usize)> {
108+
pub fn iter(&self) -> impl Iterator<Item = (&SecondaryCode, &'static str, usize)> {
110109
self.0
111110
.iter()
112-
.map(|(code, FixCount { rule_name, count })| (*code, *rule_name, *count))
111+
.map(|(code, FixCount { rule_name, count })| (code, *rule_name, *count))
113112
}
114113

115-
pub fn keys(&self) -> impl Iterator<Item = NoqaCode> {
116-
self.0.keys().copied()
114+
pub fn keys(&self) -> impl Iterator<Item = &SecondaryCode> {
115+
self.0.keys()
117116
}
118117

119118
pub fn is_empty(&self) -> bool {
120119
self.0.is_empty()
121120
}
122121
}
123122

124-
pub struct FixTableEntry<'a>(Entry<'a, NoqaCode, FixCount>);
123+
pub struct FixTableEntry<'a>(
124+
hashbrown::hash_map::EntryRef<'a, 'a, SecondaryCode, SecondaryCode, FixCount, FxBuildHasher>,
125+
);
125126

126127
impl<'a> FixTableEntry<'a> {
127128
pub fn or_default(self, rule_name: &'static str) -> &'a mut usize {
@@ -678,18 +679,16 @@ pub fn lint_fix<'a>(
678679
}
679680
}
680681

681-
fn collect_rule_codes(rules: impl IntoIterator<Item = NoqaCode>) -> String {
682-
rules
683-
.into_iter()
684-
.map(|rule| rule.to_string())
685-
.sorted_unstable()
686-
.dedup()
687-
.join(", ")
682+
fn collect_rule_codes<T>(rules: impl IntoIterator<Item = T>) -> String
683+
where
684+
T: Ord + PartialEq + std::fmt::Display,
685+
{
686+
rules.into_iter().sorted_unstable().dedup().join(", ")
688687
}
689688

690689
#[expect(clippy::print_stderr)]
691690
fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[OldDiagnostic]) {
692-
let codes = collect_rule_codes(diagnostics.iter().filter_map(OldDiagnostic::noqa_code));
691+
let codes = collect_rule_codes(diagnostics.iter().filter_map(OldDiagnostic::secondary_code));
693692
if cfg!(debug_assertions) {
694693
eprintln!(
695694
"{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
@@ -721,11 +720,11 @@ This indicates a bug in Ruff. If you could open an issue at:
721720
}
722721

723722
#[expect(clippy::print_stderr)]
724-
fn report_fix_syntax_error(
723+
fn report_fix_syntax_error<'a>(
725724
path: &Path,
726725
transformed: &str,
727726
error: &ParseError,
728-
rules: impl IntoIterator<Item = NoqaCode>,
727+
rules: impl IntoIterator<Item = &'a SecondaryCode>,
729728
) {
730729
let codes = collect_rule_codes(rules);
731730
if cfg!(debug_assertions) {

0 commit comments

Comments
 (0)